react-native-audiosprites 0.2.0-alpha.0 → 0.2.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 +109 -42
- package/lib/module/index.js +148 -40
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/index.d.ts +15 -6
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +18 -2
- package/src/index.tsx +194 -45
package/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
1
3
|
# react-native-audiosprites
|
|
2
4
|
|
|
3
5
|
A universal player for audio sprites generated by the 'audiosprite' tool.
|
|
6
|
+
Supports playing multiple sounds at same time!
|
|
4
7
|
|
|
5
8
|
## Installation
|
|
6
9
|
|
|
@@ -8,11 +11,15 @@ A universal player for audio sprites generated by the 'audiosprite' tool.
|
|
|
8
11
|
npm install react-native-audiosprites
|
|
9
12
|
```
|
|
10
13
|
|
|
14
|
+
```sh
|
|
15
|
+
yarn add react-native-audiosprites
|
|
16
|
+
```
|
|
17
|
+
|
|
11
18
|
## Usage
|
|
12
19
|
|
|
13
20
|
First, you need to generate an audio sprite and a JSON manifest file using the `audiosprite` tool.
|
|
14
21
|
|
|
15
|
-
Assuming you have `audiosprite` installed globally:
|
|
22
|
+
Assuming you have [`audiosprite`](https://www.npmjs.com/package/audiosprite) installed globally:
|
|
16
23
|
|
|
17
24
|
```sh
|
|
18
25
|
audiosprite --output audiosprite --format howler --path ./src/__tests__/ Sound_1.m4a Sound_2.m4a Sound_3.m4a Sound_4.m4a
|
|
@@ -27,13 +34,8 @@ Then, you can use the `AudioSpritePlayer` to play the sounds from the sprite.
|
|
|
27
34
|
```typescript
|
|
28
35
|
import { AudioSpritePlayer } from 'react-native-audiosprites';
|
|
29
36
|
|
|
30
|
-
// Ensure AudioContext is available
|
|
31
|
-
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
|
|
32
|
-
|
|
33
|
-
// Use the native fetch API
|
|
34
37
|
const player = new AudioSpritePlayer({
|
|
35
|
-
|
|
36
|
-
fetch: window.fetch.bind(window),
|
|
38
|
+
platform: 'web',
|
|
37
39
|
});
|
|
38
40
|
|
|
39
41
|
async function playSound(soundName: string) {
|
|
@@ -58,52 +60,117 @@ playSound('Sound_1');
|
|
|
58
60
|
|
|
59
61
|
### React Native Environment
|
|
60
62
|
|
|
61
|
-
For React Native, you'll
|
|
63
|
+
For React Native, you'll need `react-native-audio-api` and `expo-asset` to handle audio playback and asset loading.
|
|
62
64
|
|
|
63
|
-
First, install
|
|
65
|
+
First, install the dependencies:
|
|
64
66
|
|
|
65
67
|
```sh
|
|
66
|
-
npm install react-native-audio-api
|
|
68
|
+
npm install react-native-audio-api expo-asset expo-file-system
|
|
67
69
|
# or
|
|
68
|
-
yarn add react-native-audio-api
|
|
70
|
+
yarn add react-native-audio-api expo-asset expo-file-system
|
|
69
71
|
```
|
|
70
72
|
|
|
71
|
-
|
|
73
|
+
Change `metro.config.js` as per `react-native-audio-api` documentation: https://docs.swmansion.com/react-native-audio-api/docs/fundamentals/getting-started
|
|
72
74
|
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// Create an instance of AudioContext
|
|
78
|
-
const audioContext = new AudioContext();
|
|
79
|
-
|
|
80
|
-
// Use the global fetch provided by React Native
|
|
81
|
-
const player = new AudioSpritePlayer({
|
|
82
|
-
audioContext,
|
|
83
|
-
fetch: fetch, // React Native provides a global fetch
|
|
84
|
-
});
|
|
75
|
+
```js
|
|
76
|
+
module.exports = wrapWithAudioAPIMetroConfig(config);
|
|
77
|
+
```
|
|
85
78
|
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
// Load the audio sprite manifest and audio files
|
|
89
|
-
// Adjust the path to your audiosprite.json file.
|
|
90
|
-
// In React Native, you might need to bundle your audio files
|
|
91
|
-
// and refer to them using a local asset path or a remote URL.
|
|
92
|
-
// For this example, we assume it's accessible via a URL.
|
|
93
|
-
await player.load('http://localhost:8081/src/__tests__/audiosprite.json');
|
|
94
|
-
console.log('React Native Audio sprite loaded successfully.');
|
|
79
|
+
Then, you can use it in your component:
|
|
95
80
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
81
|
+
```typescript
|
|
82
|
+
import { StyleSheet, View, Text, Button, Platform } from 'react-native';
|
|
83
|
+
import { AudioSpritePlayer } from 'react-native-audiosprites';
|
|
84
|
+
import { AudioManager, AudioContext } from 'react-native-audio-api';
|
|
85
|
+
import { useEffect, useState, useRef } from 'react';
|
|
86
|
+
import { Asset } from 'expo-asset';
|
|
87
|
+
import { fetch } from 'expo/fetch';
|
|
88
|
+
import manifest from '../assets/audiosprite.json';
|
|
89
|
+
|
|
90
|
+
// Import the audio asset
|
|
91
|
+
const audioAsset = require('../assets/audiosprite.mp3');
|
|
92
|
+
|
|
93
|
+
export default function App() {
|
|
94
|
+
const [isLoaded, setIsLoaded] = useState(false);
|
|
95
|
+
const playerRef = useRef<AudioSpritePlayer | null>(null);
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
const loadPlayer = async () => {
|
|
99
|
+
const asset = Asset.fromModule(audioAsset);
|
|
100
|
+
await asset.downloadAsync();
|
|
101
|
+
const audioUri = asset.localUri || asset.uri;
|
|
102
|
+
|
|
103
|
+
if (!audioUri) {
|
|
104
|
+
console.error('Failed to get audio URI.');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (Platform.OS === 'ios') {
|
|
109
|
+
try {
|
|
110
|
+
await AudioManager.setAudioSessionOptions({
|
|
111
|
+
iosCategory: 'playback',
|
|
112
|
+
iosOptions: ['mixWithOthers'],
|
|
113
|
+
});
|
|
114
|
+
await AudioManager.setAudioSessionActivity(true);
|
|
115
|
+
} catch (e) {
|
|
116
|
+
console.error('Failed to configure AudioSession options:', e);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const audioContext = new AudioContext();
|
|
121
|
+
const audioPlayer = new AudioSpritePlayer({
|
|
122
|
+
audioContext,
|
|
123
|
+
fetch: fetch.bind(globalThis),
|
|
124
|
+
platform: Platform.OS,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
await audioPlayer.load(manifest, audioUri);
|
|
129
|
+
playerRef.current = audioPlayer;
|
|
130
|
+
setIsLoaded(true);
|
|
131
|
+
console.log('Audio sprite loaded successfully.');
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error('Failed to load audio sprite:', error);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
loadPlayer();
|
|
138
|
+
}, []);
|
|
139
|
+
|
|
140
|
+
const playSound = (soundName: string) => {
|
|
141
|
+
const player = playerRef.current;
|
|
142
|
+
if (player && isLoaded) {
|
|
143
|
+
player.play(soundName);
|
|
144
|
+
console.log(`Playing sound: ${soundName}`);
|
|
145
|
+
} else {
|
|
146
|
+
console.warn('Player not loaded yet.');
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<View style={styles.container}>
|
|
152
|
+
<Text>AudioSprite Player Example</Text>
|
|
153
|
+
<Button
|
|
154
|
+
title="Play Sound 1"
|
|
155
|
+
onPress={() => playSound('Sound_1')}
|
|
156
|
+
disabled={!isLoaded}
|
|
157
|
+
/>
|
|
158
|
+
<Button
|
|
159
|
+
title="Play Sound 2"
|
|
160
|
+
onPress={() => playSound('Sound_2')}
|
|
161
|
+
disabled={!isLoaded}
|
|
162
|
+
/>
|
|
163
|
+
</View>
|
|
164
|
+
);
|
|
102
165
|
}
|
|
103
166
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
167
|
+
const styles = StyleSheet.create({
|
|
168
|
+
container: {
|
|
169
|
+
flex: 1,
|
|
170
|
+
alignItems: 'center',
|
|
171
|
+
justifyContent: 'center',
|
|
172
|
+
},
|
|
173
|
+
});
|
|
107
174
|
```
|
|
108
175
|
|
|
109
176
|
## Contributing
|
package/lib/module/index.js
CHANGED
|
@@ -2,15 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Universal player for audio sprites generated by the 'audiosprite' tool.
|
|
5
|
-
*
|
|
5
|
+
* Requires an AudioContext and fetch to be injected.
|
|
6
|
+
* Uses AudioBufferQueueSourceNode and buffer splitting for mobile stability.
|
|
6
7
|
*/
|
|
7
8
|
export class AudioSpritePlayer {
|
|
9
|
+
// The full audio buffer
|
|
10
|
+
|
|
11
|
+
// Cache for the small, pre-split AudioBuffers used by mobile's QueueSourceNode
|
|
12
|
+
spriteBufferCache = {};
|
|
8
13
|
constructor({
|
|
9
14
|
audioContext,
|
|
10
|
-
fetch
|
|
15
|
+
fetch,
|
|
16
|
+
platform
|
|
11
17
|
}) {
|
|
12
18
|
if (!audioContext) {
|
|
13
|
-
|
|
19
|
+
if (platform === 'web') {
|
|
20
|
+
// Web doesnt need to provide AudioContext
|
|
21
|
+
// @ts-ignore
|
|
22
|
+
this.audioContext = new AudioContext();
|
|
23
|
+
} else {
|
|
24
|
+
throw new Error('An AudioContext instance must be provided from react-native-audio-api.');
|
|
25
|
+
}
|
|
14
26
|
}
|
|
15
27
|
if (!fetch) {
|
|
16
28
|
throw new Error('A fetch implementation must be provided.');
|
|
@@ -19,29 +31,101 @@ export class AudioSpritePlayer {
|
|
|
19
31
|
this.fetch = fetch;
|
|
20
32
|
this.audioBuffer = null;
|
|
21
33
|
this.manifest = null;
|
|
34
|
+
this.platform = platform; // 'web', 'ios', 'android', etc.
|
|
35
|
+
if (this.audioContext?.createBufferSource?.constructor?.name === 'AsyncFunction') {
|
|
36
|
+
console.log('createBufferSource is async! going with web default AudioContext');
|
|
37
|
+
// Can be removed after this PR gets merged
|
|
38
|
+
//https://github.com/software-mansion/react-native-audio-api/issues/574
|
|
39
|
+
// @ts-ignore
|
|
40
|
+
this.audioContext = new AudioContext();
|
|
41
|
+
}
|
|
22
42
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Caches pre-split AudioBuffers for each sprite, which is necessary
|
|
46
|
+
* for stable playback on mobile using the BufferQueueSourceNode.
|
|
47
|
+
*/
|
|
48
|
+
_cacheSpriteBuffers() {
|
|
49
|
+
if (!this.audioBuffer || !this.manifest || this.platform === 'web') {
|
|
50
|
+
return; // Only necessary for mobile platforms
|
|
51
|
+
}
|
|
52
|
+
const sampleRate = this.audioBuffer.sampleRate;
|
|
53
|
+
const numChannels = this.audioBuffer.numberOfChannels;
|
|
54
|
+
this.spriteBufferCache = {};
|
|
55
|
+
for (const soundName in this.manifest.spritemap) {
|
|
56
|
+
const sound = this.manifest.spritemap[soundName];
|
|
57
|
+
|
|
58
|
+
// Calculate frame indices
|
|
59
|
+
const startFrame = Math.floor(sound.start * sampleRate);
|
|
60
|
+
const endFrame = Math.ceil(sound.end * sampleRate);
|
|
61
|
+
const durationFrames = endFrame - startFrame;
|
|
62
|
+
if (durationFrames <= 0) {
|
|
63
|
+
console.warn(`Sprite "${soundName}" has zero or negative duration. Skipping.`);
|
|
64
|
+
continue;
|
|
28
65
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
66
|
+
|
|
67
|
+
// 1. Create a new empty buffer for the sprite
|
|
68
|
+
const spriteBuffer = this.audioContext.createBuffer(numChannels, durationFrames, sampleRate);
|
|
69
|
+
|
|
70
|
+
// 2. Copy data from the full buffer to the new sprite buffer
|
|
71
|
+
for (let i = 0; i < numChannels; i++) {
|
|
72
|
+
const sourceData = this.audioBuffer.getChannelData(i);
|
|
73
|
+
const destinationData = spriteBuffer.getChannelData(i);
|
|
74
|
+
|
|
75
|
+
// Extract the segment data (efficient array copy)
|
|
76
|
+
const segment = sourceData.subarray(startFrame, endFrame);
|
|
77
|
+
destinationData.set(segment);
|
|
32
78
|
}
|
|
79
|
+
this.spriteBufferCache[soundName] = spriteBuffer;
|
|
80
|
+
// console.log(`Cached sprite buffer for ${soundName}, frames: ${durationFrames}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async load(json, audio) {
|
|
84
|
+
try {
|
|
85
|
+
let decodedBuffer;
|
|
33
86
|
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
87
|
+
// --- Fetching and Decoding Logic (Uses existing logic) ---
|
|
88
|
+
if (typeof json === 'string') {
|
|
89
|
+
const response = await this.fetch(json);
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
throw new Error(`Failed to fetch manifest: ${response.statusText}`);
|
|
92
|
+
}
|
|
93
|
+
this.manifest = await response.json();
|
|
94
|
+
if (!this.manifest.resources || !this.manifest.spritemap) {
|
|
95
|
+
throw new Error('Invalid audiosprite manifest format. Missing "resources" or "spritemap".');
|
|
96
|
+
}
|
|
97
|
+
const audioFileName = this.manifest.resources[0];
|
|
98
|
+
const audioUrl = new URL(audioFileName, response.url).href;
|
|
99
|
+
const audioResponse = await this.fetch(audioUrl);
|
|
100
|
+
if (!audioResponse.ok) {
|
|
101
|
+
throw new Error(`Failed to fetch audio file: ${audioResponse.statusText}`);
|
|
102
|
+
}
|
|
103
|
+
const arrayBuffer = await audioResponse.arrayBuffer();
|
|
104
|
+
decodedBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
|
|
105
|
+
} else {
|
|
106
|
+
this.manifest = json;
|
|
107
|
+
if (!this.manifest.resources || !this.manifest.spritemap) {
|
|
108
|
+
throw new Error('Invalid audiosprite manifest format. Missing "resources" or "spritemap".');
|
|
109
|
+
}
|
|
110
|
+
let arrayBuffer;
|
|
111
|
+
if (typeof audio === 'string') {
|
|
112
|
+
const audioResponse = await this.fetch(audio);
|
|
113
|
+
if (!audioResponse.ok) {
|
|
114
|
+
throw new Error(`Failed to fetch audio file: ${audioResponse.statusText}`);
|
|
115
|
+
}
|
|
116
|
+
arrayBuffer = await audioResponse.arrayBuffer();
|
|
117
|
+
} else {
|
|
118
|
+
arrayBuffer = audio;
|
|
119
|
+
}
|
|
120
|
+
decodedBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
|
|
41
121
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
122
|
+
// --- End Fetching and Decoding Logic ---
|
|
123
|
+
|
|
124
|
+
this.audioBuffer = decodedBuffer;
|
|
125
|
+
|
|
126
|
+
// 🚨 CRITICAL: Split and cache buffers for mobile stability/correctness
|
|
127
|
+
this._cacheSpriteBuffers();
|
|
128
|
+
console.log('RNAS: Audio sprite loaded successfully.');
|
|
45
129
|
} catch (error) {
|
|
46
130
|
console.error('Failed to load audio sprite:', error);
|
|
47
131
|
throw error; // Re-throw for user to catch
|
|
@@ -53,38 +137,62 @@ export class AudioSpritePlayer {
|
|
|
53
137
|
return;
|
|
54
138
|
}
|
|
55
139
|
|
|
56
|
-
// Resume context if it was suspended (
|
|
140
|
+
// Resume context if it was suspended (must be non-blocking here)
|
|
57
141
|
if (this.audioContext.state === 'suspended') {
|
|
58
|
-
this.audioContext.resume()
|
|
142
|
+
this.audioContext.resume().catch(e => {
|
|
143
|
+
console.error('Failed to resume AudioContext:', e);
|
|
144
|
+
});
|
|
59
145
|
}
|
|
60
|
-
|
|
61
|
-
// Get the sound from the 'spritemap'
|
|
62
146
|
const sound = this.manifest.spritemap[soundName];
|
|
63
147
|
if (!sound) {
|
|
64
148
|
console.warn(`Sound "${soundName}" not found in spritemap.`);
|
|
65
149
|
return;
|
|
66
150
|
}
|
|
67
|
-
|
|
68
|
-
// Calculate duration from start/end times
|
|
69
151
|
const duration = sound.end - sound.start;
|
|
70
152
|
if (duration <= 0) {
|
|
71
153
|
console.warn(`Sound "${soundName}" has invalid duration.`);
|
|
72
154
|
return;
|
|
73
155
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
156
|
+
let source;
|
|
157
|
+
|
|
158
|
+
// 🚨 MOBILE LOGIC: Use AudioBufferQueueSourceNode with cached split buffer
|
|
159
|
+
if (this.platform !== 'web') {
|
|
160
|
+
const spriteBuffer = this.spriteBufferCache[soundName];
|
|
161
|
+
if (!spriteBuffer) {
|
|
162
|
+
console.error(`RNAS Error: Split buffer for "${soundName}" not found in cache.`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (!this.audioContext.createBufferQueueSource) {
|
|
166
|
+
console.error('RNAS Error: createBufferQueueSource is not available on this native platform.');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
source = this.audioContext.createBufferQueueSource();
|
|
170
|
+
|
|
171
|
+
// Mobile Implementation: Enqueue the specific, short sprite buffer
|
|
172
|
+
source.enqueueBuffer(spriteBuffer);
|
|
173
|
+
source.connect(this.audioContext.destination);
|
|
80
174
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
175
|
+
// This will play the short buffer from its start to its end.
|
|
176
|
+
source.start(1);
|
|
177
|
+
} else {
|
|
178
|
+
// 🌐 WEB LOGIC (Standard Web Audio API)
|
|
179
|
+
source = this.audioContext.createBufferSource();
|
|
180
|
+
if (!source || typeof source.connect !== 'function') {
|
|
181
|
+
console.error('RNAS Error: createBufferSource() returned an invalid object on web. Aborting playback.');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
source.buffer = this.audioBuffer;
|
|
185
|
+
source.connect(this.audioContext.destination);
|
|
186
|
+
|
|
187
|
+
// Use the 'audiosprite' format: start(when, offset, duration)
|
|
188
|
+
source.start(0,
|
|
189
|
+
// Start playing now
|
|
190
|
+
sound.start,
|
|
191
|
+
// The offset
|
|
192
|
+
duration // The calculated duration
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
console.log(`RNAS: played ${soundName} on ${this.platform}`);
|
|
88
196
|
}
|
|
89
197
|
getManifest() {
|
|
90
198
|
return this.manifest;
|
package/lib/module/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["AudioSpritePlayer","constructor","audioContext","fetch","Error","audioBuffer","manifest","
|
|
1
|
+
{"version":3,"names":["AudioSpritePlayer","spriteBufferCache","constructor","audioContext","fetch","platform","AudioContext","Error","audioBuffer","manifest","createBufferSource","name","console","log","_cacheSpriteBuffers","sampleRate","numChannels","numberOfChannels","soundName","spritemap","sound","startFrame","Math","floor","start","endFrame","ceil","end","durationFrames","warn","spriteBuffer","createBuffer","i","sourceData","getChannelData","destinationData","segment","subarray","set","load","json","audio","decodedBuffer","response","ok","statusText","resources","audioFileName","audioUrl","URL","url","href","audioResponse","arrayBuffer","decodeAudioData","error","play","state","resume","catch","e","duration","source","createBufferQueueSource","enqueueBuffer","connect","destination","buffer","getManifest","getAudioBuffer"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":"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","ignoreList":[]}
|
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Universal player for audio sprites generated by the 'audiosprite' tool.
|
|
3
|
-
*
|
|
3
|
+
* Requires an AudioContext and fetch to be injected.
|
|
4
|
+
* Uses AudioBufferQueueSourceNode and buffer splitting for mobile stability.
|
|
4
5
|
*/
|
|
5
6
|
export declare class AudioSpritePlayer {
|
|
6
|
-
audioContext: any;
|
|
7
|
-
fetch: any;
|
|
7
|
+
audioContext: any | null;
|
|
8
|
+
fetch: any | null;
|
|
8
9
|
audioBuffer: any | null;
|
|
9
10
|
manifest: any | null;
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
platform: string;
|
|
12
|
+
private spriteBufferCache;
|
|
13
|
+
constructor({ audioContext, fetch, platform, }: {
|
|
14
|
+
audioContext: any | null;
|
|
12
15
|
fetch: any;
|
|
16
|
+
platform: string;
|
|
13
17
|
});
|
|
14
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Caches pre-split AudioBuffers for each sprite, which is necessary
|
|
20
|
+
* for stable playback on mobile using the BufferQueueSourceNode.
|
|
21
|
+
*/
|
|
22
|
+
private _cacheSpriteBuffers;
|
|
23
|
+
load(json: any, audio?: any): Promise<void>;
|
|
15
24
|
play(soundName: string): void;
|
|
16
25
|
getManifest(): any;
|
|
17
26
|
getAudioBuffer(): any;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA
|
|
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;gBAExC,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;;;OAGG;IACH,OAAO,CAAC,mBAAmB;IA8CrB,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,GAAG;IAkEjC,IAAI,CAAC,SAAS,EAAE,MAAM;IA+EtB,WAAW;IAIX,cAAc;CAGf"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-audiosprites",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
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
|
@@ -1,16 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Universal player for audio sprites generated by the 'audiosprite' tool.
|
|
3
|
-
*
|
|
3
|
+
* Requires an AudioContext and fetch to be injected.
|
|
4
|
+
* Uses AudioBufferQueueSourceNode and buffer splitting for mobile stability.
|
|
4
5
|
*/
|
|
5
6
|
export class AudioSpritePlayer {
|
|
6
|
-
audioContext: any;
|
|
7
|
-
fetch: any;
|
|
8
|
-
audioBuffer: any | null;
|
|
7
|
+
audioContext: any | null;
|
|
8
|
+
fetch: any | null;
|
|
9
|
+
audioBuffer: any | null; // The full audio buffer
|
|
9
10
|
manifest: any | null;
|
|
11
|
+
platform: string;
|
|
12
|
+
// Cache for the small, pre-split AudioBuffers used by mobile's QueueSourceNode
|
|
13
|
+
private spriteBufferCache: Record<string, any> = {};
|
|
10
14
|
|
|
11
|
-
constructor({
|
|
15
|
+
constructor({
|
|
16
|
+
audioContext,
|
|
17
|
+
fetch,
|
|
18
|
+
platform,
|
|
19
|
+
}: {
|
|
20
|
+
audioContext: any | null;
|
|
21
|
+
fetch: any;
|
|
22
|
+
platform: string;
|
|
23
|
+
}) {
|
|
12
24
|
if (!audioContext) {
|
|
13
|
-
|
|
25
|
+
if (platform === 'web') {
|
|
26
|
+
// Web doesnt need to provide AudioContext
|
|
27
|
+
// @ts-ignore
|
|
28
|
+
this.audioContext = new AudioContext();
|
|
29
|
+
} else {
|
|
30
|
+
throw new Error(
|
|
31
|
+
'An AudioContext instance must be provided from react-native-audio-api.'
|
|
32
|
+
);
|
|
33
|
+
}
|
|
14
34
|
}
|
|
15
35
|
if (!fetch) {
|
|
16
36
|
throw new Error('A fetch implementation must be provided.');
|
|
@@ -19,38 +39,131 @@ export class AudioSpritePlayer {
|
|
|
19
39
|
this.fetch = fetch;
|
|
20
40
|
this.audioBuffer = null;
|
|
21
41
|
this.manifest = null;
|
|
42
|
+
this.platform = platform; // 'web', 'ios', 'android', etc.
|
|
43
|
+
if (
|
|
44
|
+
this.audioContext?.createBufferSource?.constructor?.name ===
|
|
45
|
+
'AsyncFunction'
|
|
46
|
+
) {
|
|
47
|
+
console.log(
|
|
48
|
+
'createBufferSource is async! going with web default AudioContext'
|
|
49
|
+
);
|
|
50
|
+
// Can be removed after this PR gets merged
|
|
51
|
+
//https://github.com/software-mansion/react-native-audio-api/issues/574
|
|
52
|
+
// @ts-ignore
|
|
53
|
+
this.audioContext = new AudioContext();
|
|
54
|
+
}
|
|
22
55
|
}
|
|
23
56
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Caches pre-split AudioBuffers for each sprite, which is necessary
|
|
59
|
+
* for stable playback on mobile using the BufferQueueSourceNode.
|
|
60
|
+
*/
|
|
61
|
+
private _cacheSpriteBuffers() {
|
|
62
|
+
if (!this.audioBuffer || !this.manifest || this.platform === 'web') {
|
|
63
|
+
return; // Only necessary for mobile platforms
|
|
64
|
+
}
|
|
31
65
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
66
|
+
const sampleRate = this.audioBuffer.sampleRate;
|
|
67
|
+
const numChannels = this.audioBuffer.numberOfChannels;
|
|
68
|
+
this.spriteBufferCache = {};
|
|
69
|
+
|
|
70
|
+
for (const soundName in this.manifest.spritemap) {
|
|
71
|
+
const sound = this.manifest.spritemap[soundName];
|
|
72
|
+
|
|
73
|
+
// Calculate frame indices
|
|
74
|
+
const startFrame = Math.floor(sound.start * sampleRate);
|
|
75
|
+
const endFrame = Math.ceil(sound.end * sampleRate);
|
|
76
|
+
const durationFrames = endFrame - startFrame;
|
|
77
|
+
|
|
78
|
+
if (durationFrames <= 0) {
|
|
79
|
+
console.warn(
|
|
80
|
+
`Sprite "${soundName}" has zero or negative duration. Skipping.`
|
|
35
81
|
);
|
|
82
|
+
continue;
|
|
36
83
|
}
|
|
37
84
|
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
85
|
+
// 1. Create a new empty buffer for the sprite
|
|
86
|
+
const spriteBuffer = this.audioContext.createBuffer(
|
|
87
|
+
numChannels,
|
|
88
|
+
durationFrames,
|
|
89
|
+
sampleRate
|
|
90
|
+
);
|
|
42
91
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
92
|
+
// 2. Copy data from the full buffer to the new sprite buffer
|
|
93
|
+
for (let i = 0; i < numChannels; i++) {
|
|
94
|
+
const sourceData = this.audioBuffer.getChannelData(i);
|
|
95
|
+
const destinationData = spriteBuffer.getChannelData(i);
|
|
96
|
+
|
|
97
|
+
// Extract the segment data (efficient array copy)
|
|
98
|
+
const segment = sourceData.subarray(startFrame, endFrame);
|
|
99
|
+
destinationData.set(segment);
|
|
48
100
|
}
|
|
49
101
|
|
|
50
|
-
|
|
102
|
+
this.spriteBufferCache[soundName] = spriteBuffer;
|
|
103
|
+
// console.log(`Cached sprite buffer for ${soundName}, frames: ${durationFrames}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async load(json: any, audio?: any) {
|
|
108
|
+
try {
|
|
109
|
+
let decodedBuffer: any;
|
|
110
|
+
|
|
111
|
+
// --- Fetching and Decoding Logic (Uses existing logic) ---
|
|
112
|
+
if (typeof json === 'string') {
|
|
113
|
+
const response = await this.fetch(json);
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
throw new Error(`Failed to fetch manifest: ${response.statusText}`);
|
|
116
|
+
}
|
|
117
|
+
this.manifest = await response.json();
|
|
118
|
+
|
|
119
|
+
if (!this.manifest.resources || !this.manifest.spritemap) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
'Invalid audiosprite manifest format. Missing "resources" or "spritemap".'
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const audioFileName = this.manifest.resources[0];
|
|
126
|
+
const audioUrl = new URL(audioFileName, response.url).href;
|
|
127
|
+
|
|
128
|
+
const audioResponse = await this.fetch(audioUrl);
|
|
129
|
+
if (!audioResponse.ok) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`Failed to fetch audio file: ${audioResponse.statusText}`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
51
134
|
|
|
52
|
-
|
|
53
|
-
|
|
135
|
+
const arrayBuffer = await audioResponse.arrayBuffer();
|
|
136
|
+
decodedBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
|
|
137
|
+
} else {
|
|
138
|
+
this.manifest = json;
|
|
139
|
+
if (!this.manifest.resources || !this.manifest.spritemap) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
'Invalid audiosprite manifest format. Missing "resources" or "spritemap".'
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let arrayBuffer;
|
|
146
|
+
if (typeof audio === 'string') {
|
|
147
|
+
const audioResponse = await this.fetch(audio);
|
|
148
|
+
if (!audioResponse.ok) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`Failed to fetch audio file: ${audioResponse.statusText}`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
arrayBuffer = await audioResponse.arrayBuffer();
|
|
154
|
+
} else {
|
|
155
|
+
arrayBuffer = audio;
|
|
156
|
+
}
|
|
157
|
+
decodedBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
|
|
158
|
+
}
|
|
159
|
+
// --- End Fetching and Decoding Logic ---
|
|
160
|
+
|
|
161
|
+
this.audioBuffer = decodedBuffer;
|
|
162
|
+
|
|
163
|
+
// 🚨 CRITICAL: Split and cache buffers for mobile stability/correctness
|
|
164
|
+
this._cacheSpriteBuffers();
|
|
165
|
+
|
|
166
|
+
console.log('RNAS: Audio sprite loaded successfully.');
|
|
54
167
|
} catch (error) {
|
|
55
168
|
console.error('Failed to load audio sprite:', error);
|
|
56
169
|
throw error; // Re-throw for user to catch
|
|
@@ -63,41 +176,77 @@ export class AudioSpritePlayer {
|
|
|
63
176
|
return;
|
|
64
177
|
}
|
|
65
178
|
|
|
66
|
-
// Resume context if it was suspended (
|
|
179
|
+
// Resume context if it was suspended (must be non-blocking here)
|
|
67
180
|
if (this.audioContext.state === 'suspended') {
|
|
68
|
-
this.audioContext.resume()
|
|
181
|
+
this.audioContext.resume().catch((e: any) => {
|
|
182
|
+
console.error('Failed to resume AudioContext:', e);
|
|
183
|
+
});
|
|
69
184
|
}
|
|
70
185
|
|
|
71
|
-
// Get the sound from the 'spritemap'
|
|
72
186
|
const sound = this.manifest.spritemap[soundName];
|
|
73
187
|
if (!sound) {
|
|
74
188
|
console.warn(`Sound "${soundName}" not found in spritemap.`);
|
|
75
189
|
return;
|
|
76
190
|
}
|
|
77
191
|
|
|
78
|
-
// Calculate duration from start/end times
|
|
79
192
|
const duration = sound.end - sound.start;
|
|
80
|
-
|
|
81
193
|
if (duration <= 0) {
|
|
82
194
|
console.warn(`Sound "${soundName}" has invalid duration.`);
|
|
83
195
|
return;
|
|
84
196
|
}
|
|
85
197
|
|
|
86
|
-
|
|
87
|
-
source.buffer = this.audioBuffer;
|
|
198
|
+
let source: any;
|
|
88
199
|
|
|
89
|
-
|
|
90
|
-
|
|
200
|
+
// 🚨 MOBILE LOGIC: Use AudioBufferQueueSourceNode with cached split buffer
|
|
201
|
+
if (this.platform !== 'web') {
|
|
202
|
+
const spriteBuffer = this.spriteBufferCache[soundName];
|
|
91
203
|
|
|
92
|
-
|
|
93
|
-
|
|
204
|
+
if (!spriteBuffer) {
|
|
205
|
+
console.error(
|
|
206
|
+
`RNAS Error: Split buffer for "${soundName}" not found in cache.`
|
|
207
|
+
);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!this.audioContext.createBufferQueueSource) {
|
|
212
|
+
console.error(
|
|
213
|
+
'RNAS Error: createBufferQueueSource is not available on this native platform.'
|
|
214
|
+
);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
source = this.audioContext.createBufferQueueSource();
|
|
219
|
+
|
|
220
|
+
// Mobile Implementation: Enqueue the specific, short sprite buffer
|
|
221
|
+
source.enqueueBuffer(spriteBuffer);
|
|
222
|
+
|
|
223
|
+
source.connect(this.audioContext.destination);
|
|
224
|
+
|
|
225
|
+
// This will play the short buffer from its start to its end.
|
|
226
|
+
source.start(1);
|
|
227
|
+
} else {
|
|
228
|
+
// 🌐 WEB LOGIC (Standard Web Audio API)
|
|
229
|
+
source = this.audioContext.createBufferSource();
|
|
230
|
+
|
|
231
|
+
if (!source || typeof source.connect !== 'function') {
|
|
232
|
+
console.error(
|
|
233
|
+
'RNAS Error: createBufferSource() returned an invalid object on web. Aborting playback.'
|
|
234
|
+
);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
source.buffer = this.audioBuffer;
|
|
239
|
+
source.connect(this.audioContext.destination);
|
|
240
|
+
|
|
241
|
+
// Use the 'audiosprite' format: start(when, offset, duration)
|
|
242
|
+
source.start(
|
|
243
|
+
0, // Start playing now
|
|
244
|
+
sound.start, // The offset
|
|
245
|
+
duration // The calculated duration
|
|
246
|
+
);
|
|
247
|
+
}
|
|
94
248
|
|
|
95
|
-
|
|
96
|
-
source.start(
|
|
97
|
-
0, // Start playing now
|
|
98
|
-
sound.start, // The offset
|
|
99
|
-
duration // The calculated duration
|
|
100
|
-
);
|
|
249
|
+
console.log(`RNAS: played ${soundName} on ${this.platform}`);
|
|
101
250
|
}
|
|
102
251
|
|
|
103
252
|
getManifest() {
|