Skip to content

Commit d87c540

Browse files
authored
feat: more automatic asset resolving (#869)
* feat: more automatic asset resolving * feat: cleanup web actx * fix: web - remove unnecessary number-module support for decodeAudioData
1 parent eafb17f commit d87c540

File tree

10 files changed

+172
-81
lines changed

10 files changed

+172
-81
lines changed

apps/common-app/src/examples/AudioFile/AudioFile.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import { Button, Container, Spacer } from '../../components';
1010
import { colors } from '../../styles';
1111
import AudioPlayer from './AudioPlayer';
1212

13-
const URL =
14-
'https://software-mansion.github.io/react-native-audio-api/audio/voice/example-voice-01.mp3';
13+
// const remoteAsset =
14+
// 'https://software-mansion.github.io/react-native-audio-api/audio/voice/example-voice-01.mp3';
15+
16+
import staticAsset from './voice-sample-landing.mp3';
1517

1618
const AudioFile: FC = () => {
1719
const [isPlaying, setIsPlaying] = useState(false);
@@ -56,7 +58,7 @@ const AudioFile: FC = () => {
5658
const fetchAudioBuffer = useCallback(async () => {
5759
setIsLoading(true);
5860

59-
await AudioPlayer.loadBuffer(URL);
61+
await AudioPlayer.loadBuffer(staticAsset);
6062

6163
setIsLoading(false);
6264
}, []);
@@ -83,7 +85,7 @@ const AudioFile: FC = () => {
8385
const setup = async () => {
8486
await fetchAudioBuffer();
8587
await setupNotification();
86-
}
88+
};
8789
setup();
8890
return () => {
8991
AudioPlayer.reset();
@@ -92,7 +94,6 @@ const AudioFile: FC = () => {
9294
}, [fetchAudioBuffer]);
9395

9496
useEffect(() => {
95-
9697
AudioManager.observeAudioInterruptions(true);
9798

9899
// Listen to notification control events

apps/common-app/src/examples/AudioFile/AudioPlayer.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
decodeAudioData,
88
PlaybackNotificationManager,
99
} from 'react-native-audio-api';
10-
import { Image } from 'react-native';
1110

1211
class AudioPlayer {
1312
private readonly audioContext: AudioContext;
@@ -38,7 +37,7 @@ class AudioPlayer {
3837
this.isPlaying = true;
3938
PlaybackNotificationManager.update({
4039
state: 'playing',
41-
})
40+
});
4241

4342
if (this.audioContext.state === 'suspended') {
4443
await this.audioContext.resume();
@@ -55,11 +54,16 @@ class AudioPlayer {
5554
this.sourceNode.onPositionChanged = (event) => {
5655
this.currentElapsedTime = event.value;
5756
if (this.onPositionChanged) {
58-
this.onPositionChanged(this.currentElapsedTime / this.audioBuffer!.duration);
57+
this.onPositionChanged(
58+
this.currentElapsedTime / this.audioBuffer!.duration
59+
);
5960
}
6061
};
6162

62-
this.sourceNode.start(this.audioContext.currentTime, this.currentElapsedTime);
63+
this.sourceNode.start(
64+
this.audioContext.currentTime,
65+
this.currentElapsedTime
66+
);
6367
};
6468

6569
pause = async () => {
@@ -73,7 +77,7 @@ class AudioPlayer {
7377
await this.audioContext.suspend();
7478
PlaybackNotificationManager.update({
7579
state: 'paused',
76-
})
80+
});
7781

7882
this.isPlaying = false;
7983
};
@@ -96,8 +100,13 @@ class AudioPlayer {
96100
}
97101
};
98102

99-
loadBuffer = async (url: string) => {
100-
const buffer = await decodeAudioData(url);
103+
loadBuffer = async (asset: string | number) => {
104+
const buffer = await decodeAudioData(asset, 0, {
105+
headers: {
106+
'User-Agent':
107+
'Mozilla/5.0 (Android; Mobile; rv:122.0) Gecko/122.0 Firefox/122.0',
108+
},
109+
});
101110

102111
if (buffer) {
103112
this.audioBuffer = buffer;
@@ -132,7 +141,7 @@ class AudioPlayer {
132141

133142
getElapsedTime = (): number => {
134143
return this.currentElapsedTime;
135-
}
144+
};
136145
}
137146

138147
export default new AudioPlayer();
968 KB
Binary file not shown.

apps/common-app/types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module '*.mp3';

packages/react-native-audio-api/src/core/AudioDecoder.ts

Lines changed: 88 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1+
import { Image } from 'react-native';
2+
13
import { IAudioDecoder } from '../interfaces';
4+
import { DecodeDataInput } from '../types';
5+
import {
6+
isBase64Source,
7+
isDataBlobString,
8+
isRemoteSource,
9+
} from '../utils/paths';
210
import AudioBuffer from './AudioBuffer';
311

412
class AudioDecoder {
@@ -9,50 +17,89 @@ class AudioDecoder {
917
this.decoder = global.createAudioDecoder();
1018
}
1119

20+
private async decodeAudioDataImplementation(
21+
input: DecodeDataInput,
22+
sampleRate?: number,
23+
fetchOptions?: RequestInit
24+
): Promise<AudioBuffer | null | undefined> {
25+
if (input instanceof ArrayBuffer) {
26+
const buffer = await this.decoder.decodeWithMemoryBlock(
27+
new Uint8Array(input),
28+
sampleRate ?? 0
29+
);
30+
return new AudioBuffer(buffer);
31+
}
32+
33+
const stringSource =
34+
typeof input === 'number' ? Image.resolveAssetSource(input).uri : input;
35+
36+
// input is data:audio/...;base64,...
37+
if (isBase64Source(stringSource)) {
38+
throw new Error(
39+
'Base64 source decoding is not currently supported, to decode raw PCM base64 strings use decodePCMInBase64 method.'
40+
);
41+
}
42+
43+
// input is blob:...
44+
if (isDataBlobString(stringSource)) {
45+
throw new Error('Data Blob string decoding is not currently supported.');
46+
}
47+
48+
// input is http(s)://...
49+
if (isRemoteSource(stringSource)) {
50+
const arrayBuffer = await fetch(stringSource, fetchOptions).then((res) =>
51+
res.arrayBuffer()
52+
);
53+
54+
const buffer = await this.decoder.decodeWithMemoryBlock(
55+
new Uint8Array(arrayBuffer),
56+
sampleRate ?? 0
57+
);
58+
59+
return new AudioBuffer(buffer);
60+
}
61+
62+
if (!(typeof input === 'string')) {
63+
throw new TypeError('Input must be a module, uri or ArrayBuffer');
64+
}
65+
66+
// Local file path
67+
const filePath = stringSource.startsWith('file://')
68+
? stringSource.replace('file://', '')
69+
: stringSource;
70+
71+
const buffer = await this.decoder.decodeWithFilePath(
72+
filePath,
73+
sampleRate ?? 0
74+
);
75+
76+
return new AudioBuffer(buffer);
77+
}
78+
1279
public static getInstance(): AudioDecoder {
1380
if (!AudioDecoder.instance) {
1481
AudioDecoder.instance = new AudioDecoder();
1582
}
83+
1684
return AudioDecoder.instance;
1785
}
1886

1987
public async decodeAudioDataInstance(
20-
input: string | ArrayBuffer,
21-
sampleRate?: number
88+
input: DecodeDataInput,
89+
sampleRate?: number,
90+
fetchOptions?: RequestInit
2291
): Promise<AudioBuffer> {
23-
let buffer;
24-
if (typeof input === 'string') {
25-
// Remove the file:// prefix if it exists
26-
if (input.startsWith('file://')) {
27-
input = input.replace('file://', '');
28-
} else if (input.startsWith('https://') || input.startsWith('http://')) {
29-
// For remote URLs, we need to fetch the data first
30-
const response = await fetch(input, {
31-
headers: {
32-
'User-Agent':
33-
'Mozilla/5.0 (Android; Mobile; rv:122.0) Gecko/122.0 Firefox/122.0',
34-
},
35-
})
36-
.then((res) => res.arrayBuffer())
37-
.then((arrayBuffer) => arrayBuffer);
38-
buffer = await this.decoder.decodeWithMemoryBlock(
39-
new Uint8Array(response),
40-
sampleRate ?? 0
41-
);
42-
} else {
43-
buffer = await this.decoder.decodeWithFilePath(input, sampleRate ?? 0);
44-
}
45-
} else if (input instanceof ArrayBuffer) {
46-
buffer = await this.decoder.decodeWithMemoryBlock(
47-
new Uint8Array(input),
48-
sampleRate ?? 0
49-
);
50-
}
92+
const audioBuffer = await this.decodeAudioDataImplementation(
93+
input,
94+
sampleRate,
95+
fetchOptions
96+
);
5197

52-
if (!buffer) {
53-
throw new Error('Unsupported input type or failed to decode audio');
98+
if (!audioBuffer) {
99+
throw new Error('Failed to decode audio data.');
54100
}
55-
return new AudioBuffer(buffer);
101+
102+
return audioBuffer;
56103
}
57104

58105
public async decodePCMInBase64Instance(
@@ -72,10 +119,15 @@ class AudioDecoder {
72119
}
73120

74121
export async function decodeAudioData(
75-
input: string | ArrayBuffer,
76-
sampleRate?: number
122+
input: DecodeDataInput,
123+
sampleRate?: number,
124+
fetchOptions?: RequestInit
77125
): Promise<AudioBuffer> {
78-
return AudioDecoder.getInstance().decodeAudioDataInstance(input, sampleRate);
126+
return AudioDecoder.getInstance().decodeAudioDataInstance(
127+
input,
128+
sampleRate,
129+
fetchOptions
130+
);
79131
}
80132

81133
export async function decodePCMInBase64(

packages/react-native-audio-api/src/core/BaseAudioContext.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
AudioWorkletRuntime,
1111
ContextState,
1212
ConvolverNodeOptions,
13+
DecodeDataInput,
1314
IIRFilterNodeOptions,
1415
PeriodicWaveConstraints,
1516
} from '../types';
@@ -56,13 +57,10 @@ export default class BaseAudioContext {
5657
}
5758

5859
public async decodeAudioData(
59-
input: string | ArrayBuffer,
60-
sampleRate?: number
60+
input: DecodeDataInput,
61+
fetchOptions?: RequestInit
6162
): Promise<AudioBuffer> {
62-
if (!(typeof input === 'string' || input instanceof ArrayBuffer)) {
63-
throw new TypeError('Input must be a string or ArrayBuffer');
64-
}
65-
return await decodeAudioData(input, sampleRate ?? this.sampleRate);
63+
return await decodeAudioData(input, this.sampleRate, fetchOptions);
6664
}
6765

6866
public async decodePCMInBase64(

packages/react-native-audio-api/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,5 @@ export interface IIRFilterNodeOptions {
149149
feedforward: number[];
150150
feedback: number[];
151151
}
152+
153+
export type DecodeDataInput = number | string | ArrayBuffer;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export function isRemoteSource(url: string): boolean {
2+
return url.startsWith('http://') || url.startsWith('https://');
3+
}
4+
5+
export function isBase64Source(data: string): boolean {
6+
return data.startsWith('data:audio/') && data.includes(';base64,');
7+
}
8+
9+
export function isDataBlobString(data: string): boolean {
10+
return data.startsWith('blob:');
11+
}

packages/react-native-audio-api/src/web-core/AudioContext.tsx

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,29 @@
1+
import { InvalidAccessError, NotSupportedError } from '../errors';
12
import {
2-
ContextState,
3-
PeriodicWaveConstraints,
4-
AudioContextOptions,
53
AudioBufferBaseSourceNodeOptions,
4+
AudioContextOptions,
5+
ContextState,
6+
DecodeDataInput,
67
IIRFilterNodeOptions,
8+
PeriodicWaveConstraints,
79
} from '../types';
8-
import { InvalidAccessError, NotSupportedError } from '../errors';
9-
import BaseAudioContext from './BaseAudioContext';
1010
import AnalyserNode from './AnalyserNode';
11-
import AudioDestinationNode from './AudioDestinationNode';
1211
import AudioBuffer from './AudioBuffer';
1312
import AudioBufferSourceNode from './AudioBufferSourceNode';
13+
import AudioDestinationNode from './AudioDestinationNode';
14+
import BaseAudioContext from './BaseAudioContext';
1415
import BiquadFilterNode from './BiquadFilterNode';
15-
import IIRFilterNode from './IIRFilterNode';
16+
import ConvolverNode from './ConvolverNode';
17+
import { ConvolverNodeOptions } from './ConvolverNodeOptions';
18+
import DelayNode from './DelayNode';
1619
import GainNode from './GainNode';
20+
import IIRFilterNode from './IIRFilterNode';
1721
import OscillatorNode from './OscillatorNode';
1822
import PeriodicWave from './PeriodicWave';
1923
import StereoPannerNode from './StereoPannerNode';
20-
import ConvolverNode from './ConvolverNode';
21-
import DelayNode from './DelayNode';
22-
import { ConvolverNodeOptions } from './ConvolverNodeOptions';
2324

24-
import { globalWasmPromise, globalTag } from './custom/LoadCustomWasm';
2525
import ConstantSourceNode from './ConstantSourceNode';
26+
import { globalTag, globalWasmPromise } from './custom/LoadCustomWasm';
2627
import WaveShaperNode from './WaveShaperNode';
2728

2829
export default class AudioContext implements BaseAudioContext {
@@ -180,16 +181,30 @@ export default class AudioContext implements BaseAudioContext {
180181
return new WaveShaperNode(this, this.context.createWaveShaper());
181182
}
182183

183-
async decodeAudioDataSource(source: string): Promise<AudioBuffer> {
184-
const arrayBuffer = await fetch(source).then((response) =>
185-
response.arrayBuffer()
186-
);
184+
async decodeAudioData(
185+
source: DecodeDataInput,
186+
fetchOptions?: RequestInit
187+
): Promise<AudioBuffer> {
188+
if (source instanceof ArrayBuffer) {
189+
const decodedData = await this.context.decodeAudioData(source);
190+
return new AudioBuffer(decodedData);
191+
}
187192

188-
return this.decodeAudioData(arrayBuffer);
189-
}
193+
if (typeof source === 'string') {
194+
const response = await fetch(source, fetchOptions);
195+
196+
if (!response.ok) {
197+
throw new InvalidAccessError(
198+
`Failed to fetch audio data from the provided source: ${source}`
199+
);
200+
}
201+
202+
const arrayBuffer = await response.arrayBuffer();
203+
const decodedData = await this.context.decodeAudioData(arrayBuffer);
204+
return new AudioBuffer(decodedData);
205+
}
190206

191-
async decodeAudioData(arrayBuffer: ArrayBuffer): Promise<AudioBuffer> {
192-
return new AudioBuffer(await this.context.decodeAudioData(arrayBuffer));
207+
throw new TypeError('Unsupported source for decodeAudioData: ' + source);
193208
}
194209

195210
async close(): Promise<void> {

0 commit comments

Comments
 (0)