react-native-audiosprites 0.2.0-alpha.0 → 0.2.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 CHANGED
@@ -1,6 +1,9 @@
1
+ ![react-native-audiosprites_poster](react-native-audiosprites_poster.jpg)
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
- audioContext,
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 typically need a polyfill or a library that provides `AudioContext` and `fetch` functionality, as these are Web APIs. `react-native-audio-api` is a good option for `AudioContext`. For `fetch`, React Native provides its own global `fetch` implementation.
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 `react-native-audio-api`:
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
- Then, you can use it like this:
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
- ```typescript
74
- import { AudioSpritePlayer } from 'react-native-audiosprites';
75
- import { AudioContext } from 'react-native-audio-api'; // Import from the library
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
- async function playRNSound(soundName: string) {
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
- // Play a sound from the spritemap
97
- player.play(soundName);
98
- console.log(`Playing React Native sound: ${soundName}`);
99
- } catch (error) {
100
- console.error('Error playing React Native sound:', error);
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
- // Example usage:
105
- playRNSound('Sound_1');
106
- // playRNSound('Sound_2');
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
@@ -2,15 +2,27 @@
2
2
 
3
3
  /**
4
4
  * Universal player for audio sprites generated by the 'audiosprite' tool.
5
- * It requires an AudioContext and fetch to be injected.
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
- throw new Error('An AudioContext instance must be provided.');
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
- async load(jsonPath) {
24
- try {
25
- const response = await this.fetch(jsonPath);
26
- if (!response.ok) {
27
- throw new Error(`Failed to fetch manifest: ${response.statusText}`);
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
- this.manifest = await response.json();
30
- if (!this.manifest.resources || !this.manifest.spritemap) {
31
- throw new Error('Invalid audiosprite manifest format. Missing "resources" or "spritemap".');
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
- // Find the first supported audio file (e.g., .mp3 or .ogg)
35
- // For simplicity, we just take the first one.
36
- const audioFileName = this.manifest.resources[0];
37
- const audioUrl = new URL(audioFileName, response.url).href;
38
- const audioResponse = await this.fetch(audioUrl);
39
- if (!audioResponse.ok) {
40
- throw new Error(`Failed to fetch audio file: ${audioResponse.statusText}`);
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
- const arrayBuffer = await audioResponse.arrayBuffer();
43
- this.audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
44
- console.log('Audio sprite loaded successfully.');
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 (e.g., by browser policy)
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
- const source = this.audioContext.createBufferSource();
75
- source.buffer = this.audioBuffer;
76
- const gain = this.audioContext.createGain();
77
- gain.gain.setValueAtTime(1.0, this.audioContext.currentTime);
78
- source.connect(gain);
79
- gain.connect(this.audioContext.destination);
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
- // Use the 'audiosprite' format: start(when, offset, duration)
82
- source.start(0,
83
- // Start playing now
84
- sound.start,
85
- // The offset
86
- duration // The calculated duration
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;
@@ -1 +1 @@
1
- {"version":3,"names":["AudioSpritePlayer","constructor","audioContext","fetch","Error","audioBuffer","manifest","load","jsonPath","response","ok","statusText","json","resources","spritemap","audioFileName","audioUrl","URL","url","href","audioResponse","arrayBuffer","decodeAudioData","console","log","error","play","soundName","warn","state","resume","sound","duration","end","start","source","createBufferSource","buffer","gain","createGain","setValueAtTime","currentTime","connect","destination","getManifest","getAudioBuffer"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":"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","ignoreList":[]}
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":";;AAAA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMA,iBAAiB,CAAC;EAGJ;;EAGzB;EACQC,iBAAiB,GAAwB,CAAC,CAAC;EAEnDC,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;EACUQ,mBAAmBA,CAAA,EAAG;IAC5B,IAAI,CAAC,IAAI,CAACN,WAAW,IAAI,CAAC,IAAI,CAACC,QAAQ,IAAI,IAAI,CAACJ,QAAQ,KAAK,KAAK,EAAE;MAClE,OAAO,CAAC;IACV;IAEA,MAAMU,UAAU,GAAG,IAAI,CAACP,WAAW,CAACO,UAAU;IAC9C,MAAMC,WAAW,GAAG,IAAI,CAACR,WAAW,CAACS,gBAAgB;IACrD,IAAI,CAAChB,iBAAiB,GAAG,CAAC,CAAC;IAE3B,KAAK,MAAMiB,SAAS,IAAI,IAAI,CAACT,QAAQ,CAACU,SAAS,EAAE;MAC/C,MAAMC,KAAK,GAAG,IAAI,CAACX,QAAQ,CAACU,SAAS,CAACD,SAAS,CAAC;;MAEhD;MACA,MAAMG,UAAU,GAAGC,IAAI,CAACC,KAAK,CAACH,KAAK,CAACI,KAAK,GAAGT,UAAU,CAAC;MACvD,MAAMU,QAAQ,GAAGH,IAAI,CAACI,IAAI,CAACN,KAAK,CAACO,GAAG,GAAGZ,UAAU,CAAC;MAClD,MAAMa,cAAc,GAAGH,QAAQ,GAAGJ,UAAU;MAE5C,IAAIO,cAAc,IAAI,CAAC,EAAE;QACvBhB,OAAO,CAACiB,IAAI,CACV,WAAWX,SAAS,4CACtB,CAAC;QACD;MACF;;MAEA;MACA,MAAMY,YAAY,GAAG,IAAI,CAAC3B,YAAY,CAAC4B,YAAY,CACjDf,WAAW,EACXY,cAAc,EACdb,UACF,CAAC;;MAED;MACA,KAAK,IAAIiB,CAAC,GAAG,CAAC,EAAEA,CAAC,GAAGhB,WAAW,EAAEgB,CAAC,EAAE,EAAE;QACpC,MAAMC,UAAU,GAAG,IAAI,CAACzB,WAAW,CAAC0B,cAAc,CAACF,CAAC,CAAC;QACrD,MAAMG,eAAe,GAAGL,YAAY,CAACI,cAAc,CAACF,CAAC,CAAC;;QAEtD;QACA,MAAMI,OAAO,GAAGH,UAAU,CAACI,QAAQ,CAAChB,UAAU,EAAEI,QAAQ,CAAC;QACzDU,eAAe,CAACG,GAAG,CAACF,OAAO,CAAC;MAC9B;MAEA,IAAI,CAACnC,iBAAiB,CAACiB,SAAS,CAAC,GAAGY,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,CAACvC,KAAK,CAACoC,IAAI,CAAC;QACvC,IAAI,CAACG,QAAQ,CAACC,EAAE,EAAE;UAChB,MAAM,IAAIrC,KAAK,CAAC,6BAA6BoC,QAAQ,CAACE,UAAU,EAAE,CAAC;QACrE;QACA,IAAI,CAACpC,QAAQ,GAAG,MAAMkC,QAAQ,CAACH,IAAI,CAAC,CAAC;QAErC,IAAI,CAAC,IAAI,CAAC/B,QAAQ,CAACqC,SAAS,IAAI,CAAC,IAAI,CAACrC,QAAQ,CAACU,SAAS,EAAE;UACxD,MAAM,IAAIZ,KAAK,CACb,0EACF,CAAC;QACH;QAEA,MAAMwC,aAAa,GAAG,IAAI,CAACtC,QAAQ,CAACqC,SAAS,CAAC,CAAC,CAAC;QAChD,MAAME,QAAQ,GAAG,IAAIC,GAAG,CAACF,aAAa,EAAEJ,QAAQ,CAACO,GAAG,CAAC,CAACC,IAAI;QAE1D,MAAMC,aAAa,GAAG,MAAM,IAAI,CAAChD,KAAK,CAAC4C,QAAQ,CAAC;QAChD,IAAI,CAACI,aAAa,CAACR,EAAE,EAAE;UACrB,MAAM,IAAIrC,KAAK,CACb,+BAA+B6C,aAAa,CAACP,UAAU,EACzD,CAAC;QACH;QAEA,MAAMQ,WAAW,GAAG,MAAMD,aAAa,CAACC,WAAW,CAAC,CAAC;QACrDX,aAAa,GAAG,MAAM,IAAI,CAACvC,YAAY,CAACmD,eAAe,CAACD,WAAW,CAAC;MACtE,CAAC,MAAM;QACL,IAAI,CAAC5C,QAAQ,GAAG+B,IAAI;QACpB,IAAI,CAAC,IAAI,CAAC/B,QAAQ,CAACqC,SAAS,IAAI,CAAC,IAAI,CAACrC,QAAQ,CAACU,SAAS,EAAE;UACxD,MAAM,IAAIZ,KAAK,CACb,0EACF,CAAC;QACH;QAEA,IAAI8C,WAAW;QACf,IAAI,OAAOZ,KAAK,KAAK,QAAQ,EAAE;UAC7B,MAAMW,aAAa,GAAG,MAAM,IAAI,CAAChD,KAAK,CAACqC,KAAK,CAAC;UAC7C,IAAI,CAACW,aAAa,CAACR,EAAE,EAAE;YACrB,MAAM,IAAIrC,KAAK,CACb,+BAA+B6C,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,CAACvC,YAAY,CAACmD,eAAe,CAACD,WAAW,CAAC;MACtE;MACA;;MAEA,IAAI,CAAC7C,WAAW,GAAGkC,aAAa;;MAEhC;MACA,IAAI,CAAC5B,mBAAmB,CAAC,CAAC;MAE1BF,OAAO,CAACC,GAAG,CAAC,yCAAyC,CAAC;IACxD,CAAC,CAAC,OAAO0C,KAAK,EAAE;MACd3C,OAAO,CAAC2C,KAAK,CAAC,8BAA8B,EAAEA,KAAK,CAAC;MACpD,MAAMA,KAAK,CAAC,CAAC;IACf;EACF;EAEAC,IAAIA,CAACtC,SAAiB,EAAE;IACtB,IAAI,CAAC,IAAI,CAACV,WAAW,IAAI,CAAC,IAAI,CAACC,QAAQ,EAAE;MACvCG,OAAO,CAACiB,IAAI,CAAC,6CAA6C,CAAC;MAC3D;IACF;;IAEA;IACA,IAAI,IAAI,CAAC1B,YAAY,CAACsD,KAAK,KAAK,WAAW,EAAE;MAC3C,IAAI,CAACtD,YAAY,CAACuD,MAAM,CAAC,CAAC,CAACC,KAAK,CAAEC,CAAM,IAAK;QAC3ChD,OAAO,CAAC2C,KAAK,CAAC,gCAAgC,EAAEK,CAAC,CAAC;MACpD,CAAC,CAAC;IACJ;IAEA,MAAMxC,KAAK,GAAG,IAAI,CAACX,QAAQ,CAACU,SAAS,CAACD,SAAS,CAAC;IAChD,IAAI,CAACE,KAAK,EAAE;MACVR,OAAO,CAACiB,IAAI,CAAC,UAAUX,SAAS,2BAA2B,CAAC;MAC5D;IACF;IAEA,MAAM2C,QAAQ,GAAGzC,KAAK,CAACO,GAAG,GAAGP,KAAK,CAACI,KAAK;IACxC,IAAIqC,QAAQ,IAAI,CAAC,EAAE;MACjBjD,OAAO,CAACiB,IAAI,CAAC,UAAUX,SAAS,yBAAyB,CAAC;MAC1D;IACF;IAEA,IAAI4C,MAAW;;IAEf;IACA,IAAI,IAAI,CAACzD,QAAQ,KAAK,KAAK,EAAE;MAC3B,MAAMyB,YAAY,GAAG,IAAI,CAAC7B,iBAAiB,CAACiB,SAAS,CAAC;MAEtD,IAAI,CAACY,YAAY,EAAE;QACjBlB,OAAO,CAAC2C,KAAK,CACX,iCAAiCrC,SAAS,uBAC5C,CAAC;QACD;MACF;MAEA,IAAI,CAAC,IAAI,CAACf,YAAY,CAAC4D,uBAAuB,EAAE;QAC9CnD,OAAO,CAAC2C,KAAK,CACX,+EACF,CAAC;QACD;MACF;MAEAO,MAAM,GAAG,IAAI,CAAC3D,YAAY,CAAC4D,uBAAuB,CAAC,CAAC;;MAEpD;MACAD,MAAM,CAACE,aAAa,CAAClC,YAAY,CAAC;MAElCgC,MAAM,CAACG,OAAO,CAAC,IAAI,CAAC9D,YAAY,CAAC+D,WAAW,CAAC;;MAE7C;MACAJ,MAAM,CAACtC,KAAK,CAAC,CAAC,CAAC;IACjB,CAAC,MAAM;MACL;MACAsC,MAAM,GAAG,IAAI,CAAC3D,YAAY,CAACO,kBAAkB,CAAC,CAAC;MAE/C,IAAI,CAACoD,MAAM,IAAI,OAAOA,MAAM,CAACG,OAAO,KAAK,UAAU,EAAE;QACnDrD,OAAO,CAAC2C,KAAK,CACX,wFACF,CAAC;QACD;MACF;MAEAO,MAAM,CAACK,MAAM,GAAG,IAAI,CAAC3D,WAAW;MAChCsD,MAAM,CAACG,OAAO,CAAC,IAAI,CAAC9D,YAAY,CAAC+D,WAAW,CAAC;;MAE7C;MACAJ,MAAM,CAACtC,KAAK,CACV,CAAC;MAAE;MACHJ,KAAK,CAACI,KAAK;MAAE;MACbqC,QAAQ,CAAC;MACX,CAAC;IACH;IAEAjD,OAAO,CAACC,GAAG,CAAC,gBAAgBK,SAAS,OAAO,IAAI,CAACb,QAAQ,EAAE,CAAC;EAC9D;EAEA+D,WAAWA,CAAA,EAAG;IACZ,OAAO,IAAI,CAAC3D,QAAQ;EACtB;EAEA4D,cAAcA,CAAA,EAAG;IACf,OAAO,IAAI,CAAC7D,WAAW;EACzB;AACF","ignoreList":[]}
@@ -1,17 +1,26 @@
1
1
  /**
2
2
  * Universal player for audio sprites generated by the 'audiosprite' tool.
3
- * It requires an AudioContext and fetch to be injected.
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
- constructor({ audioContext, fetch }: {
11
- audioContext: any;
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
- load(jsonPath: string): Promise<void>;
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;;;GAGG;AACH,qBAAa,iBAAiB;IAC5B,YAAY,EAAE,GAAG,CAAC;IAClB,KAAK,EAAE,GAAG,CAAC;IACX,WAAW,EAAE,GAAG,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,GAAG,GAAG,IAAI,CAAC;gBAET,EAAE,YAAY,EAAE,KAAK,EAAE,EAAE;QAAE,YAAY,EAAE,GAAG,CAAC;QAAC,KAAK,EAAE,GAAG,CAAA;KAAE;IAahE,IAAI,CAAC,QAAQ,EAAE,MAAM;IAoC3B,IAAI,CAAC,SAAS,EAAE,MAAM;IA2CtB,WAAW;IAIX,cAAc;CAGf"}
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.0-alpha.0",
3
+ "version": "0.2.0",
4
4
  "description": "audio sprites ",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
package/src/index.tsx CHANGED
@@ -1,16 +1,36 @@
1
1
  /**
2
2
  * Universal player for audio sprites generated by the 'audiosprite' tool.
3
- * It requires an AudioContext and fetch to be injected.
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({ audioContext, fetch }: { audioContext: any; fetch: any }) {
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
- throw new Error('An AudioContext instance must be provided.');
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
- async load(jsonPath: string) {
25
- try {
26
- const response = await this.fetch(jsonPath);
27
- if (!response.ok) {
28
- throw new Error(`Failed to fetch manifest: ${response.statusText}`);
29
- }
30
- this.manifest = await response.json();
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
- if (!this.manifest.resources || !this.manifest.spritemap) {
33
- throw new Error(
34
- 'Invalid audiosprite manifest format. Missing "resources" or "spritemap".'
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
- // Find the first supported audio file (e.g., .mp3 or .ogg)
39
- // For simplicity, we just take the first one.
40
- const audioFileName = this.manifest.resources[0];
41
- const audioUrl = new URL(audioFileName, response.url).href;
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
- const audioResponse = await this.fetch(audioUrl);
44
- if (!audioResponse.ok) {
45
- throw new Error(
46
- `Failed to fetch audio file: ${audioResponse.statusText}`
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
- const arrayBuffer = await audioResponse.arrayBuffer();
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
- this.audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
53
- console.log('Audio sprite loaded successfully.');
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 (e.g., by browser policy)
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
- const source = this.audioContext.createBufferSource();
87
- source.buffer = this.audioBuffer;
198
+ let source: any;
88
199
 
89
- const gain = this.audioContext.createGain();
90
- gain.gain.setValueAtTime(1.0, this.audioContext.currentTime);
200
+ // 🚨 MOBILE LOGIC: Use AudioBufferQueueSourceNode with cached split buffer
201
+ if (this.platform !== 'web') {
202
+ const spriteBuffer = this.spriteBufferCache[soundName];
91
203
 
92
- source.connect(gain);
93
- gain.connect(this.audioContext.destination);
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
- // Use the 'audiosprite' format: start(when, offset, duration)
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() {