Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions apps/common-app/src/examples/AudioFile/AudioFile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import { Button, Container, Spacer } from '../../components';
import { colors } from '../../styles';
import AudioPlayer from './AudioPlayer';

const URL =
'https://software-mansion.github.io/react-native-audio-api/audio/voice/example-voice-01.mp3';
// const remoteAsset =
// 'https://software-mansion.github.io/react-native-audio-api/audio/voice/example-voice-01.mp3';

import staticAsset from './voice-sample-landing.mp3';

const AudioFile: FC = () => {
const [isPlaying, setIsPlaying] = useState(false);
Expand Down Expand Up @@ -56,7 +58,7 @@ const AudioFile: FC = () => {
const fetchAudioBuffer = useCallback(async () => {
setIsLoading(true);

await AudioPlayer.loadBuffer(URL);
await AudioPlayer.loadBuffer(staticAsset);

setIsLoading(false);
}, []);
Expand All @@ -83,7 +85,7 @@ const AudioFile: FC = () => {
const setup = async () => {
await fetchAudioBuffer();
await setupNotification();
}
};
setup();
return () => {
AudioPlayer.reset();
Expand All @@ -92,7 +94,6 @@ const AudioFile: FC = () => {
}, [fetchAudioBuffer]);

useEffect(() => {

AudioManager.observeAudioInterruptions(true);

// Listen to notification control events
Expand Down
25 changes: 17 additions & 8 deletions apps/common-app/src/examples/AudioFile/AudioPlayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
decodeAudioData,
PlaybackNotificationManager,
} from 'react-native-audio-api';
import { Image } from 'react-native';

class AudioPlayer {
private readonly audioContext: AudioContext;
Expand Down Expand Up @@ -38,7 +37,7 @@ class AudioPlayer {
this.isPlaying = true;
PlaybackNotificationManager.update({
state: 'playing',
})
});

if (this.audioContext.state === 'suspended') {
await this.audioContext.resume();
Expand All @@ -55,11 +54,16 @@ class AudioPlayer {
this.sourceNode.onPositionChanged = (event) => {
this.currentElapsedTime = event.value;
if (this.onPositionChanged) {
this.onPositionChanged(this.currentElapsedTime / this.audioBuffer!.duration);
this.onPositionChanged(
this.currentElapsedTime / this.audioBuffer!.duration
);
}
};

this.sourceNode.start(this.audioContext.currentTime, this.currentElapsedTime);
this.sourceNode.start(
this.audioContext.currentTime,
this.currentElapsedTime
);
};

pause = async () => {
Expand All @@ -73,7 +77,7 @@ class AudioPlayer {
await this.audioContext.suspend();
PlaybackNotificationManager.update({
state: 'paused',
})
});

this.isPlaying = false;
};
Expand All @@ -96,8 +100,13 @@ class AudioPlayer {
}
};

loadBuffer = async (url: string) => {
const buffer = await decodeAudioData(url);
loadBuffer = async (asset: string | number) => {
const buffer = await decodeAudioData(asset, 0, {
headers: {
'User-Agent':
'Mozilla/5.0 (Android; Mobile; rv:122.0) Gecko/122.0 Firefox/122.0',
},
});

if (buffer) {
this.audioBuffer = buffer;
Expand Down Expand Up @@ -132,7 +141,7 @@ class AudioPlayer {

getElapsedTime = (): number => {
return this.currentElapsedTime;
}
};
}

export default new AudioPlayer();
Binary file not shown.
1 change: 1 addition & 0 deletions apps/common-app/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module '*.mp3';
124 changes: 88 additions & 36 deletions packages/react-native-audio-api/src/core/AudioDecoder.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { Image } from 'react-native';

import { IAudioDecoder } from '../interfaces';
import { DecodeDataInput } from '../types';
import {
isBase64Source,
isDataBlobString,
isRemoteSource,
} from '../utils/paths';
import AudioBuffer from './AudioBuffer';

class AudioDecoder {
Expand All @@ -9,50 +17,89 @@ class AudioDecoder {
this.decoder = global.createAudioDecoder();
}

private async decodeAudioDataImplementation(
input: DecodeDataInput,
sampleRate?: number,
fetchOptions?: RequestInit
): Promise<AudioBuffer | null | undefined> {
if (input instanceof ArrayBuffer) {
const buffer = await this.decoder.decodeWithMemoryBlock(
new Uint8Array(input),
sampleRate ?? 0
);
return new AudioBuffer(buffer);
}

const stringSource =
typeof input === 'number' ? Image.resolveAssetSource(input).uri : input;

// input is data:audio/...;base64,...
if (isBase64Source(stringSource)) {
throw new Error(
'Base64 source decoding is not currently supported, to decode raw PCM base64 strings use decodePCMInBase64 method.'
);
}

// input is blob:...
if (isDataBlobString(stringSource)) {
throw new Error('Data Blob string decoding is not currently supported.');
}

// input is http(s)://...
if (isRemoteSource(stringSource)) {
const arrayBuffer = await fetch(stringSource, fetchOptions).then((res) =>
res.arrayBuffer()
);

const buffer = await this.decoder.decodeWithMemoryBlock(
new Uint8Array(arrayBuffer),
sampleRate ?? 0
);

return new AudioBuffer(buffer);
}

if (!(typeof input === 'string')) {
throw new TypeError('Input must be a module, uri or ArrayBuffer');
}

// Local file path
const filePath = stringSource.startsWith('file://')
? stringSource.replace('file://', '')
: stringSource;

const buffer = await this.decoder.decodeWithFilePath(
filePath,
sampleRate ?? 0
);

return new AudioBuffer(buffer);
}

public static getInstance(): AudioDecoder {
if (!AudioDecoder.instance) {
AudioDecoder.instance = new AudioDecoder();
}

return AudioDecoder.instance;
}

public async decodeAudioDataInstance(
input: string | ArrayBuffer,
sampleRate?: number
input: DecodeDataInput,
sampleRate?: number,
fetchOptions?: RequestInit
): Promise<AudioBuffer> {
let buffer;
if (typeof input === 'string') {
// Remove the file:// prefix if it exists
if (input.startsWith('file://')) {
input = input.replace('file://', '');
} else if (input.startsWith('https://') || input.startsWith('http://')) {
// For remote URLs, we need to fetch the data first
const response = await fetch(input, {
headers: {
'User-Agent':
'Mozilla/5.0 (Android; Mobile; rv:122.0) Gecko/122.0 Firefox/122.0',
},
})
.then((res) => res.arrayBuffer())
.then((arrayBuffer) => arrayBuffer);
buffer = await this.decoder.decodeWithMemoryBlock(
new Uint8Array(response),
sampleRate ?? 0
);
} else {
buffer = await this.decoder.decodeWithFilePath(input, sampleRate ?? 0);
}
} else if (input instanceof ArrayBuffer) {
buffer = await this.decoder.decodeWithMemoryBlock(
new Uint8Array(input),
sampleRate ?? 0
);
}
const audioBuffer = await this.decodeAudioDataImplementation(
input,
sampleRate,
fetchOptions
);

if (!buffer) {
throw new Error('Unsupported input type or failed to decode audio');
if (!audioBuffer) {
throw new Error('Failed to decode audio data.');
}
return new AudioBuffer(buffer);

return audioBuffer;
}

public async decodePCMInBase64Instance(
Expand All @@ -72,10 +119,15 @@ class AudioDecoder {
}

export async function decodeAudioData(
input: string | ArrayBuffer,
sampleRate?: number
input: DecodeDataInput,
sampleRate?: number,
fetchOptions?: RequestInit
): Promise<AudioBuffer> {
return AudioDecoder.getInstance().decodeAudioDataInstance(input, sampleRate);
return AudioDecoder.getInstance().decodeAudioDataInstance(
input,
sampleRate,
fetchOptions
);
}

export async function decodePCMInBase64(
Expand Down
10 changes: 4 additions & 6 deletions packages/react-native-audio-api/src/core/BaseAudioContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
AudioWorkletRuntime,
ContextState,
ConvolverNodeOptions,
DecodeDataInput,
IIRFilterNodeOptions,
PeriodicWaveConstraints,
} from '../types';
Expand Down Expand Up @@ -56,13 +57,10 @@ export default class BaseAudioContext {
}

public async decodeAudioData(
input: string | ArrayBuffer,
sampleRate?: number
input: DecodeDataInput,
fetchOptions?: RequestInit
): Promise<AudioBuffer> {
if (!(typeof input === 'string' || input instanceof ArrayBuffer)) {
throw new TypeError('Input must be a string or ArrayBuffer');
}
return await decodeAudioData(input, sampleRate ?? this.sampleRate);
return await decodeAudioData(input, this.sampleRate, fetchOptions);
}

public async decodePCMInBase64(
Expand Down
2 changes: 2 additions & 0 deletions packages/react-native-audio-api/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,5 @@ export interface IIRFilterNodeOptions {
feedforward: number[];
feedback: number[];
}

export type DecodeDataInput = number | string | ArrayBuffer;
11 changes: 11 additions & 0 deletions packages/react-native-audio-api/src/utils/paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export function isRemoteSource(url: string): boolean {
return url.startsWith('http://') || url.startsWith('https://');
}

export function isBase64Source(data: string): boolean {
return data.startsWith('data:audio/') && data.includes(';base64,');
}

export function isDataBlobString(data: string): boolean {
return data.startsWith('blob:');
}
53 changes: 34 additions & 19 deletions packages/react-native-audio-api/src/web-core/AudioContext.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
import { InvalidAccessError, NotSupportedError } from '../errors';
import {
ContextState,
PeriodicWaveConstraints,
AudioContextOptions,
AudioBufferBaseSourceNodeOptions,
AudioContextOptions,
ContextState,
DecodeDataInput,
IIRFilterNodeOptions,
PeriodicWaveConstraints,
} from '../types';
import { InvalidAccessError, NotSupportedError } from '../errors';
import BaseAudioContext from './BaseAudioContext';
import AnalyserNode from './AnalyserNode';
import AudioDestinationNode from './AudioDestinationNode';
import AudioBuffer from './AudioBuffer';
import AudioBufferSourceNode from './AudioBufferSourceNode';
import AudioDestinationNode from './AudioDestinationNode';
import BaseAudioContext from './BaseAudioContext';
import BiquadFilterNode from './BiquadFilterNode';
import IIRFilterNode from './IIRFilterNode';
import ConvolverNode from './ConvolverNode';
import { ConvolverNodeOptions } from './ConvolverNodeOptions';
import DelayNode from './DelayNode';
import GainNode from './GainNode';
import IIRFilterNode from './IIRFilterNode';
import OscillatorNode from './OscillatorNode';
import PeriodicWave from './PeriodicWave';
import StereoPannerNode from './StereoPannerNode';
import ConvolverNode from './ConvolverNode';
import DelayNode from './DelayNode';
import { ConvolverNodeOptions } from './ConvolverNodeOptions';

import { globalWasmPromise, globalTag } from './custom/LoadCustomWasm';
import ConstantSourceNode from './ConstantSourceNode';
import { globalTag, globalWasmPromise } from './custom/LoadCustomWasm';
import WaveShaperNode from './WaveShaperNode';

export default class AudioContext implements BaseAudioContext {
Expand Down Expand Up @@ -180,16 +181,30 @@ export default class AudioContext implements BaseAudioContext {
return new WaveShaperNode(this, this.context.createWaveShaper());
}

async decodeAudioDataSource(source: string): Promise<AudioBuffer> {
const arrayBuffer = await fetch(source).then((response) =>
response.arrayBuffer()
);
async decodeAudioData(
source: DecodeDataInput,
fetchOptions?: RequestInit
): Promise<AudioBuffer> {
if (source instanceof ArrayBuffer) {
const decodedData = await this.context.decodeAudioData(source);
return new AudioBuffer(decodedData);
}

return this.decodeAudioData(arrayBuffer);
}
if (typeof source === 'string') {
const response = await fetch(source, fetchOptions);

if (!response.ok) {
throw new InvalidAccessError(
`Failed to fetch audio data from the provided source: ${source}`
);
}

const arrayBuffer = await response.arrayBuffer();
const decodedData = await this.context.decodeAudioData(arrayBuffer);
return new AudioBuffer(decodedData);
}

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

async close(): Promise<void> {
Expand Down
Loading