diff --git a/packages/audiodocs/docs/other/testing.mdx b/packages/audiodocs/docs/other/testing.mdx
new file mode 100644
index 000000000..febb96b2d
--- /dev/null
+++ b/packages/audiodocs/docs/other/testing.mdx
@@ -0,0 +1,375 @@
+
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# Testing
+
+React Native Audio API provides a comprehensive mock implementation to help you test your audio-related code without requiring actual audio hardware or platform-specific implementations.
+
+## Mock Implementation
+
+The mock implementation provides the same API surface as the real library but with no-op or simplified implementations that are perfect for unit testing.
+
+### Importing Mocks
+
+
+
+
+```typescript
+import * as MockAudioAPI from 'react-native-audio-api/mock';
+
+// Or import specific components
+import { AudioContext, AudioRecorder } from 'react-native-audio-api/mock';
+```
+
+
+
+
+```typescript
+// In your test setup file
+jest.mock('react-native-audio-api', () =>
+ require('react-native-audio-api/mock')
+);
+
+// Then in your tests
+import { AudioContext, AudioRecorder } from 'react-native-audio-api';
+```
+
+
+
+
+## Basic Usage
+
+### Audio Context Testing
+
+```typescript
+import { AudioContext } from 'react-native-audio-api/mock';
+
+describe('Audio Graph Tests', () => {
+ it('should create and connect audio nodes', () => {
+ const context = new AudioContext();
+
+ // Create nodes
+ const oscillator = context.createOscillator();
+ const gainNode = context.createGain();
+
+ // Configure properties
+ oscillator.frequency.value = 440; // A4 note
+ gainNode.gain.value = 0.5; // 50% volume
+
+ // Connect the audio graph
+ oscillator.connect(gainNode);
+ gainNode.connect(context.destination);
+
+ // Test the configuration
+ expect(oscillator.frequency.value).toBe(440);
+ expect(gainNode.gain.value).toBe(0.5);
+ });
+
+ it('should support context state management', async () => {
+ const context = new AudioContext();
+ expect(context.state).toBe('running');
+
+ await context.suspend();
+ expect(context.state).toBe('suspended');
+
+ await context.resume();
+ expect(context.state).toBe('running');
+ });
+});
+```
+
+### Audio Recording Testing
+
+```typescript
+import { AudioContext, AudioRecorder, FileFormat, FileDirectory } from 'react-native-audio-api/mock';
+
+describe('Audio Recording Tests', () => {
+ it('should configure and control recording', () => {
+ const context = new AudioContext();
+ const recorder = new AudioRecorder();
+
+ // Configure file output
+ const result = recorder.enableFileOutput({
+ format: FileFormat.M4A,
+ channelCount: 2,
+ directory: FileDirectory.Document,
+ });
+
+ expect(result.status).toBe('success');
+
+ // Set up recording chain
+ const oscillator = context.createOscillator();
+ const recorderAdapter = context.createRecorderAdapter();
+
+ oscillator.connect(recorderAdapter);
+ recorder.connect(recorderAdapter);
+
+ // Test recording workflow
+ const startResult = recorder.start();
+ expect(startResult.status).toBe('success');
+ expect(recorder.isRecording()).toBe(true);
+
+ const stopResult = recorder.stop();
+ expect(stopResult.status).toBe('success');
+ expect(recorder.isRecording()).toBe(false);
+ });
+});
+```
+
+### Offline Audio Processing
+
+```typescript
+import { OfflineAudioContext } from 'react-native-audio-api/mock';
+
+describe('Offline Processing Tests', () => {
+ it('should render offline audio', async () => {
+ const offlineContext = new OfflineAudioContext({
+ numberOfChannels: 2,
+ length: 44100, // 1 second at 44.1kHz
+ sampleRate: 44100,
+ });
+
+ // Create a simple tone
+ const oscillator = offlineContext.createOscillator();
+ oscillator.frequency.value = 440;
+ oscillator.connect(offlineContext.destination);
+
+ // Render the audio
+ const renderedBuffer = await offlineContext.startRendering();
+
+ expect(renderedBuffer.numberOfChannels).toBe(2);
+ expect(renderedBuffer.length).toBe(44100);
+ expect(renderedBuffer.sampleRate).toBe(44100);
+ });
+});
+```
+
+## Advanced Testing Scenarios
+
+### Custom Worklet Testing
+
+```typescript
+import { AudioContext, WorkletProcessingNode } from 'react-native-audio-api/mock';
+
+describe('Worklet Tests', () => {
+ it('should create custom audio processing', () => {
+ const context = new AudioContext();
+
+ const processingCallback = jest.fn((inputData, outputData, framesToProcess) => {
+ // Mock audio processing logic
+ for (let channel = 0; channel < outputData.length; channel++) {
+ for (let i = 0; i < framesToProcess; i++) {
+ outputData[channel][i] = inputData[channel][i] * 0.5; // Simple gain
+ }
+ }
+ });
+
+ const workletNode = new WorkletProcessingNode(
+ context,
+ 'AudioRuntime',
+ processingCallback
+ );
+
+ expect(workletNode.context).toBe(context);
+ });
+});
+```
+
+### Audio Streaming Testing
+
+```typescript
+import { AudioContext } from 'react-native-audio-api/mock';
+
+describe('Streaming Tests', () => {
+ it('should handle audio streaming', () => {
+ const context = new AudioContext();
+
+ const streamer = context.createStreamer({
+ streamPath: 'https://example.com/audio-stream',
+ });
+
+ expect(streamer.streamPath).toBe('https://example.com/audio-stream');
+
+ // Test streaming controls
+ streamer.start();
+ streamer.pause();
+ streamer.resume();
+ streamer.stop();
+ });
+});
+```
+
+### Error Handling Testing
+
+```typescript
+import {
+ AudioRecorder,
+ NotSupportedError,
+ InvalidStateError
+} from 'react-native-audio-api/mock';
+
+describe('Error Handling Tests', () => {
+ it('should handle various error conditions', () => {
+ // Test error creation
+ expect(() => {
+ throw new NotSupportedError('Feature not supported');
+ }).toThrow('Feature not supported');
+
+ // Test recorder connection errors
+ const recorder = new AudioRecorder();
+ const context = new AudioContext();
+ const adapter = context.createRecorderAdapter();
+
+ // First connection should work
+ recorder.connect(adapter);
+
+ // Second connection should throw
+ expect(() => recorder.connect(adapter)).toThrow();
+ });
+});
+```
+
+## Mock Configuration
+
+### System Volume Testing
+
+```typescript
+import { useSystemVolume, setMockSystemVolume, AudioManager } from 'react-native-audio-api/mock';
+
+describe('System Integration Tests', () => {
+ it('should mock system audio management', () => {
+ // Test system sample rate
+ const preferredRate = AudioManager.getDevicePreferredSampleRate();
+ expect(preferredRate).toBe(44100);
+
+ // Test volume management
+ setMockSystemVolume(0.7);
+ const currentVolume = useSystemVolume();
+ expect(currentVolume).toBe(0.7);
+
+ // Test event listeners
+ const volumeCallback = jest.fn();
+ const listener = AudioManager.addSystemEventListener(
+ 'volumeChange',
+ volumeCallback
+ );
+
+ expect(listener.remove).toBeDefined();
+ listener.remove();
+ });
+});
+```
+
+### Audio Callback Testing
+
+```typescript
+import { AudioRecorder } from 'react-native-audio-api/mock';
+
+describe('Callback Tests', () => {
+ it('should handle audio data callbacks', () => {
+ const recorder = new AudioRecorder();
+ const audioDataCallback = jest.fn();
+
+ const result = recorder.onAudioReady(
+ {
+ sampleRate: 44100,
+ bufferLength: 1024,
+ channelCount: 2,
+ },
+ audioDataCallback
+ );
+
+ expect(result.status).toBe('success');
+
+ // Test callback cleanup
+ recorder.clearOnAudioReady();
+ expect(() => recorder.clearOnAudioReady()).not.toThrow();
+ });
+});
+```
+
+## Type Safety
+
+The mock implementation provides full TypeScript support with the same types as the real library:
+
+```typescript
+import type { AudioContext, AudioParam, GainNode } from 'react-native-audio-api/mock';
+
+// All types are available and identical to the real implementation
+function processAudioNode(node: GainNode): void {
+ node.gain.value = 0.5;
+}
+```
+
+## Testing Best Practices
+
+1. **Isolate Audio Logic**: Test audio processing logic separately from UI components
+2. **Mock External Dependencies**: Use mocks for file system, network, and platform-specific operations
+3. **Test Error Scenarios**: Verify your code handles various error conditions gracefully
+4. **Validate Audio Graph Structure**: Ensure nodes are connected correctly
+5. **Test Async Operations**: Use proper async/await patterns for operations like rendering
+
+## Example Test Suite
+
+```typescript
+import {
+ AudioContext,
+ AudioRecorder,
+ FileFormat,
+ decodeAudioData
+} from 'react-native-audio-api/mock';
+
+describe('Audio Application Tests', () => {
+ let context: AudioContext;
+
+ beforeEach(() => {
+ context = new AudioContext();
+ });
+
+ afterEach(() => {
+ // Clean up if needed
+ context.close();
+ });
+
+ describe('Audio Graph', () => {
+ it('should create complex audio processing chain', () => {
+ const oscillator = context.createOscillator();
+ const filter = context.createBiquadFilter();
+ const delay = context.createDelay();
+ const gainNode = context.createGain();
+
+ // Configure effects chain
+ filter.type = 'lowpass';
+ filter.frequency.value = 2000;
+ delay.delayTime.value = 0.3;
+ gainNode.gain.value = 0.8;
+
+ // Connect the chain
+ oscillator.connect(filter);
+ filter.connect(delay);
+ delay.connect(gainNode);
+ gainNode.connect(context.destination);
+
+ // Verify configuration
+ expect(filter.type).toBe('lowpass');
+ expect(delay.delayTime.value).toBe(0.3);
+ expect(gainNode.gain.value).toBe(0.8);
+ });
+ });
+
+ describe('File Operations', () => {
+ it('should handle audio file processing', async () => {
+ const mockAudioData = new ArrayBuffer(1024);
+
+ // Test audio decoding
+ const decodedBuffer = await decodeAudioData(mockAudioData);
+ expect(decodedBuffer.numberOfChannels).toBe(2);
+ expect(decodedBuffer.sampleRate).toBe(44100);
+ });
+ });
+});
+```
+
+The mock implementation provides a complete testing environment that allows you to thoroughly test your audio applications without requiring real audio hardware or complex setup.
diff --git a/packages/audiodocs/yarn.lock b/packages/audiodocs/yarn.lock
index a9d662725..947ea0fef 100644
--- a/packages/audiodocs/yarn.lock
+++ b/packages/audiodocs/yarn.lock
@@ -13232,10 +13232,10 @@ typescript-eslint@^8.18.0:
"@typescript-eslint/typescript-estree" "8.41.0"
"@typescript-eslint/utils" "8.41.0"
-typescript@~5.2.2:
- version "5.2.2"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78"
- integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==
+typescript@~5.8.3:
+ version "5.8.3"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e"
+ integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==
ua-parser-js@^1.0.35:
version "1.0.41"
diff --git a/packages/react-native-audio-api/mock/package.json b/packages/react-native-audio-api/mock/package.json
new file mode 100644
index 000000000..d31d1d373
--- /dev/null
+++ b/packages/react-native-audio-api/mock/package.json
@@ -0,0 +1,6 @@
+{
+ "main": "../../lib/module/mock/index",
+ "module": "../../lib/module/mock/index",
+ "react-native": "../../src/mock/index",
+ "types": "../../lib/typescript/mock/index.d.ts"
+}
diff --git a/packages/react-native-audio-api/package.json b/packages/react-native-audio-api/package.json
index 8816ac54e..f1ed96881 100644
--- a/packages/react-native-audio-api/package.json
+++ b/packages/react-native-audio-api/package.json
@@ -13,6 +13,7 @@
"files": [
"src/",
"development/react/",
+ "mock/",
"lib/",
"common/",
"android/src/main/AndroidManifest.xml",
diff --git a/packages/react-native-audio-api/src/core/StereoPannerNode.ts b/packages/react-native-audio-api/src/core/StereoPannerNode.ts
index 040e9f840..e0fc787be 100644
--- a/packages/react-native-audio-api/src/core/StereoPannerNode.ts
+++ b/packages/react-native-audio-api/src/core/StereoPannerNode.ts
@@ -1,6 +1,6 @@
+import { StereoPannerOptions } from '../defaults';
import { IStereoPannerNode } from '../interfaces';
-import { SteroPannerOptions } from '../defaults';
-import { TSteroPannerOptions } from '../types';
+import { TStereoPannerOptions } from '../types';
import AudioNode from './AudioNode';
import AudioParam from './AudioParam';
import BaseAudioContext from './BaseAudioContext';
@@ -8,9 +8,9 @@ import BaseAudioContext from './BaseAudioContext';
export default class StereoPannerNode extends AudioNode {
readonly pan: AudioParam;
- constructor(context: BaseAudioContext, options?: TSteroPannerOptions) {
- const finalOptions: TSteroPannerOptions = {
- ...SteroPannerOptions,
+ constructor(context: BaseAudioContext, options?: TStereoPannerOptions) {
+ const finalOptions: TStereoPannerOptions = {
+ ...StereoPannerOptions,
...options,
};
const pan: IStereoPannerNode =
diff --git a/packages/react-native-audio-api/src/defaults.ts b/packages/react-native-audio-api/src/defaults.ts
index e40433ddb..cfda045e2 100644
--- a/packages/react-native-audio-api/src/defaults.ts
+++ b/packages/react-native-audio-api/src/defaults.ts
@@ -1,17 +1,17 @@
import {
- TAudioNodeOptions,
- TGainOptions,
- TSteroPannerOptions,
- TConvolverOptions,
- TConstantSourceOptions,
- TPeriodicWaveConstraints,
TAnalyserOptions,
- TBiquadFilterOptions,
- TOscillatorOptions,
- TBaseAudioBufferSourceOptions,
- TAudioBufferSourceOptions,
TAudioBufferOptions,
+ TAudioBufferSourceOptions,
+ TAudioNodeOptions,
+ TBaseAudioBufferSourceOptions,
+ TBiquadFilterOptions,
+ TConstantSourceOptions,
+ TConvolverOptions,
TDelayOptions,
+ TGainOptions,
+ TOscillatorOptions,
+ TPeriodicWaveConstraints,
+ TStereoPannerOptions,
TWaveShaperOptions,
} from './types';
@@ -26,7 +26,7 @@ export const GainOptions: TGainOptions = {
gain: 1,
};
-export const SteroPannerOptions: TSteroPannerOptions = {
+export const StereoPannerOptions: TStereoPannerOptions = {
...AudioNodeOptions,
channelCountMode: 'clamped-max',
pan: 0,
diff --git a/packages/react-native-audio-api/src/interfaces.ts b/packages/react-native-audio-api/src/interfaces.ts
index 24f05b93b..9f9df8a1d 100644
--- a/packages/react-native-audio-api/src/interfaces.ts
+++ b/packages/react-native-audio-api/src/interfaces.ts
@@ -10,21 +10,21 @@ import type {
OscillatorType,
OverSampleType,
Result,
- WindowType,
- TGainOptions,
- TSteroPannerOptions,
- TConvolverOptions,
- TConstantSourceOptions,
TAnalyserOptions,
- TBiquadFilterOptions,
- TOscillatorOptions,
- TBaseAudioBufferSourceOptions,
- TAudioBufferSourceOptions,
- TStreamerOptions,
TAudioBufferOptions,
+ TAudioBufferSourceOptions,
+ TBaseAudioBufferSourceOptions,
+ TBiquadFilterOptions,
+ TConstantSourceOptions,
+ TConvolverOptions,
TDelayOptions,
+ TGainOptions,
TIIRFilterOptions,
+ TOscillatorOptions,
+ TStereoPannerOptions,
+ TStreamerOptions,
TWaveShaperOptions,
+ WindowType,
} from './types';
// IMPORTANT: use only IClass, because it is a part of contract between cpp host object and js layer
@@ -82,7 +82,7 @@ export interface IBaseAudioContext {
): IConstantSourceNode;
createGain(gainOptions: TGainOptions): IGainNode;
createStereoPanner(
- stereoPannerOptions: TSteroPannerOptions
+ stereoPannerOptions: TStereoPannerOptions
): IStereoPannerNode;
createBiquadFilter: (
biquadFilterOptions: TBiquadFilterOptions
diff --git a/packages/react-native-audio-api/src/mock/index.ts b/packages/react-native-audio-api/src/mock/index.ts
new file mode 100644
index 000000000..62554fdd9
--- /dev/null
+++ b/packages/react-native-audio-api/src/mock/index.ts
@@ -0,0 +1,1165 @@
+import {
+ AudioContextOptions,
+ AudioRecorderCallbackOptions,
+ AudioRecorderFileOptions,
+ AudioRecorderStartOptions,
+ AudioWorkletRuntime,
+ BiquadFilterType,
+ ChannelCountMode,
+ ChannelInterpretation,
+ ContextState,
+ FileDirectory,
+ FileFormat,
+ FileInfo,
+ FilePresetType,
+ OfflineAudioContextOptions,
+ OscillatorType,
+ OverSampleType,
+ Result,
+ TAnalyserOptions,
+ TAudioBufferSourceOptions,
+ TBaseAudioBufferSourceOptions,
+ TBiquadFilterOptions,
+ TConstantSourceOptions,
+ TConvolverOptions,
+ TDelayOptions,
+ TGainOptions,
+ TOscillatorOptions,
+ TPeriodicWaveOptions,
+ TStereoPannerOptions,
+ TStreamerOptions,
+ TWaveShaperOptions,
+} from '../types';
+
+/* eslint-disable no-useless-constructor */
+
+const noop = () => {};
+
+class MockEventSubscription {
+ public subscriptionId: string = Number(1).toString();
+
+ remove = noop;
+}
+
+class MockAudioEventEmitter {
+ private listeners: {
+ [event: string]: Array<
+ (event: Partial> | undefined) => void
+ >;
+ } = {};
+
+ addAudioEventListener(
+ event: string,
+ callback: (event: Partial> | undefined) => void
+ ): MockEventSubscription {
+ if (!this.listeners[event]) {
+ this.listeners[event] = [];
+ }
+ this.listeners[event].push(callback);
+ const subscription = new MockEventSubscription();
+
+ subscription.remove = () => {
+ const index = this.listeners[event]?.indexOf(callback);
+ if (index && index > -1) {
+ this.listeners[event].splice(index, 1);
+ }
+ };
+
+ return subscription;
+ }
+
+ emit(event: string, data: Record): void {
+ this.listeners[event]?.forEach((callback) => callback(data));
+ }
+}
+
+class AudioParamMock {
+ private _value: number = 0;
+ public defaultValue: number = 0;
+ public minValue: number = -3.4028235e38;
+ public maxValue: number = 3.4028235e38;
+
+ constructor(_audioParam: unknown, _context: BaseAudioContextMock) {}
+
+ get value(): number {
+ return this._value;
+ }
+
+ set value(newValue: number) {
+ this._value = newValue;
+ }
+
+ public setValueAtTime(value: number, _startTime: number): AudioParamMock {
+ this._value = value;
+ return this;
+ }
+
+ public linearRampToValueAtTime(
+ value: number,
+ _endTime: number
+ ): AudioParamMock {
+ this._value = value;
+ return this;
+ }
+
+ public exponentialRampToValueAtTime(
+ value: number,
+ _endTime: number
+ ): AudioParamMock {
+ this._value = value;
+ return this;
+ }
+
+ public setTargetAtTime(
+ target: number,
+ _startTime: number,
+ _timeConstant: number
+ ): AudioParamMock {
+ this._value = target;
+ return this;
+ }
+
+ public setValueCurveAtTime(
+ _values: Float32Array,
+ _startTime: number,
+ _duration: number
+ ): AudioParamMock {
+ return this;
+ }
+
+ public cancelScheduledValues(_startTime: number): AudioParamMock {
+ return this;
+ }
+
+ public cancelAndHoldAtTime(_startTime: number): AudioParamMock {
+ return this;
+ }
+}
+
+class AudioBufferMock {
+ public sampleRate: number;
+ public length: number;
+ public duration: number;
+ public numberOfChannels: number;
+
+ constructor(options: {
+ numberOfChannels: number;
+ length: number;
+ sampleRate: number;
+ }) {
+ this.numberOfChannels = options.numberOfChannels;
+ this.length = options.length;
+ this.sampleRate = options.sampleRate;
+ this.duration = options.length / options.sampleRate;
+ }
+
+ getChannelData(_channel: number): Float32Array {
+ return new Float32Array(this.length);
+ }
+
+ copyFromChannel(
+ _destination: Float32Array,
+ _channelNumber: number,
+ _startInChannel?: number
+ ): void {}
+
+ copyToChannel(
+ _source: Float32Array,
+ _channelNumber: number,
+ _startInChannel?: number
+ ): void {}
+}
+
+class AudioNodeMock {
+ public context: BaseAudioContextMock;
+ public numberOfInputs: number = 1;
+ public numberOfOutputs: number = 1;
+ public channelCount: number = 2;
+ public channelCountMode: ChannelCountMode = 'max';
+ public channelInterpretation: ChannelInterpretation = 'speakers';
+
+ constructor(context: BaseAudioContextMock, _node: unknown) {
+ this.context = context;
+ }
+
+ public connect(
+ destination: AudioNodeMock | AudioParamMock
+ ): AudioNodeMock | void {
+ if (destination instanceof AudioParamMock) {
+ return;
+ }
+ return destination;
+ }
+
+ public disconnect(_destination?: AudioNodeMock): void {}
+}
+
+class AudioScheduledSourceNodeMock extends AudioNodeMock {
+ private _onended: ((event: Event) => void) | null = null;
+
+ constructor(context: BaseAudioContextMock, node: unknown) {
+ super(context, node);
+ }
+
+ public start(_when: number = 0): void {}
+ public stop(_when: number = 0): void {}
+
+ public get onended(): ((event: Event) => void) | null {
+ return this._onended;
+ }
+
+ public set onended(callback: ((event: Event) => void) | null) {
+ this._onended = callback;
+ }
+}
+
+class AnalyserNodeMock extends AudioNodeMock {
+ public fftSize: number = 2048;
+ public frequencyBinCount: number = 1024;
+ public minDecibels: number = -100;
+ public maxDecibels: number = -30;
+ public smoothingTimeConstant: number = 0.8;
+
+ constructor(context: BaseAudioContextMock, _options?: TAnalyserOptions) {
+ super(context, {});
+ }
+
+ getByteFrequencyData(_array: Uint8Array): void {}
+ getByteTimeDomainData(_array: Uint8Array): void {}
+ getFloatFrequencyData(_array: Float32Array): void {}
+ getFloatTimeDomainData(_array: Float32Array): void {}
+}
+
+class GainNodeMock extends AudioNodeMock {
+ readonly gain: AudioParamMock;
+
+ constructor(context: BaseAudioContextMock, _options?: TGainOptions) {
+ super(context, {});
+ this.gain = new AudioParamMock({}, context);
+ this.gain.value = 1;
+ }
+}
+
+class DelayNodeMock extends AudioNodeMock {
+ readonly delayTime: AudioParamMock;
+
+ constructor(context: BaseAudioContextMock, _options?: TDelayOptions) {
+ super(context, {});
+ this.delayTime = new AudioParamMock({}, context);
+ this.delayTime.maxValue = 1;
+ }
+}
+
+class BiquadFilterNodeMock extends AudioNodeMock {
+ private _type: BiquadFilterType = 'lowpass';
+ readonly frequency: AudioParamMock;
+ readonly detune: AudioParamMock;
+ readonly Q: AudioParamMock;
+ readonly gain: AudioParamMock;
+
+ constructor(context: BaseAudioContextMock, _options?: TBiquadFilterOptions) {
+ super(context, {});
+ this.frequency = new AudioParamMock({}, context);
+ this.detune = new AudioParamMock({}, context);
+ this.Q = new AudioParamMock({}, context);
+ this.gain = new AudioParamMock({}, context);
+
+ this.frequency.value = 350;
+ this.Q.value = 1;
+ this.gain.value = 0;
+ }
+
+ get type(): BiquadFilterType {
+ return this._type;
+ }
+
+ set type(value: BiquadFilterType) {
+ this._type = value;
+ }
+
+ getFrequencyResponse(
+ _frequencyHz: Float32Array,
+ _magResponse: Float32Array,
+ _phaseResponse: Float32Array
+ ): void {}
+}
+
+class ConvolverNodeMock extends AudioNodeMock {
+ private _buffer: AudioBufferMock | null = null;
+ public normalize: boolean = true;
+
+ constructor(context: BaseAudioContextMock, _options?: TConvolverOptions) {
+ super(context, {});
+ }
+
+ get buffer(): AudioBufferMock | null {
+ return this._buffer;
+ }
+
+ set buffer(value: AudioBufferMock | null) {
+ this._buffer = value;
+ }
+}
+
+class WaveShaperNodeMock extends AudioNodeMock {
+ private _curve: Float32Array | null = null;
+ private _oversample: OverSampleType = 'none';
+
+ constructor(context: BaseAudioContextMock, _options?: TWaveShaperOptions) {
+ super(context, {});
+ }
+
+ get curve(): Float32Array | null {
+ return this._curve;
+ }
+
+ set curve(value: Float32Array | null) {
+ this._curve = value;
+ }
+
+ get oversample(): OverSampleType {
+ return this._oversample;
+ }
+
+ set oversample(value: OverSampleType) {
+ this._oversample = value;
+ }
+}
+
+class StereoPannerNodeMock extends AudioNodeMock {
+ readonly pan: AudioParamMock;
+
+ constructor(context: BaseAudioContextMock, _options?: TStereoPannerOptions) {
+ super(context, {});
+ this.pan = new AudioParamMock({}, context);
+ }
+}
+
+class OscillatorNodeMock extends AudioScheduledSourceNodeMock {
+ private _type: OscillatorType = 'sine';
+ readonly frequency: AudioParamMock;
+ readonly detune: AudioParamMock;
+
+ constructor(context: BaseAudioContextMock, _options?: TOscillatorOptions) {
+ super(context, {});
+ this.frequency = new AudioParamMock({}, context);
+ this.detune = new AudioParamMock({}, context);
+ this.frequency.value = 440;
+ }
+
+ get type(): OscillatorType {
+ return this._type;
+ }
+
+ set type(value: OscillatorType) {
+ this._type = value;
+ }
+
+ public setPeriodicWave(_wave: PeriodicWaveMock): void {}
+}
+
+class ConstantSourceNodeMock extends AudioScheduledSourceNodeMock {
+ readonly offset: AudioParamMock;
+
+ constructor(
+ context: BaseAudioContextMock,
+ _options?: TConstantSourceOptions
+ ) {
+ super(context, {});
+ this.offset = new AudioParamMock({}, context);
+ this.offset.value = 1;
+ }
+}
+
+class AudioBufferSourceNodeMock extends AudioScheduledSourceNodeMock {
+ private _buffer: AudioBufferMock | null = null;
+ private _loop: boolean = false;
+ private _loopStart: number = 0;
+ private _loopEnd: number = 0;
+ readonly playbackRate: AudioParamMock;
+
+ constructor(
+ context: BaseAudioContextMock,
+ _options?: TAudioBufferSourceOptions
+ ) {
+ super(context, {});
+ this.playbackRate = new AudioParamMock({}, context);
+ this.playbackRate.value = 1;
+ }
+
+ get buffer(): AudioBufferMock | null {
+ return this._buffer;
+ }
+
+ set buffer(value: AudioBufferMock | null) {
+ this._buffer = value;
+ }
+
+ get loop(): boolean {
+ return this._loop;
+ }
+
+ set loop(value: boolean) {
+ this._loop = value;
+ }
+
+ get loopStart(): number {
+ return this._loopStart;
+ }
+
+ set loopStart(value: number) {
+ this._loopStart = value;
+ }
+
+ get loopEnd(): number {
+ return this._loopEnd;
+ }
+
+ set loopEnd(value: number) {
+ this._loopEnd = value;
+ }
+}
+
+class RecorderAdapterNodeMock extends AudioNodeMock {
+ public wasConnected: boolean = false;
+
+ constructor(context: BaseAudioContextMock) {
+ super(context, {});
+ }
+
+ getNode(): Record {
+ return {};
+ }
+}
+
+class AudioBufferQueueSourceNodeMock extends AudioScheduledSourceNodeMock {
+ private _onBufferEnded: ((event: { bufferId: string }) => void) | null = null;
+ private eventEmitter = new MockAudioEventEmitter();
+
+ constructor(
+ context: BaseAudioContextMock,
+ _options?: TBaseAudioBufferSourceOptions
+ ) {
+ super(context, {});
+ }
+
+ enqueueBuffer(_buffer: AudioBufferMock): string {
+ return Math.random().toString(36).substr(2, 9);
+ }
+
+ dequeueBuffer(_bufferId: string): void {}
+ clearBuffers(): void {}
+ pause(): void {}
+
+ get onBufferEnded(): ((event: { bufferId: string }) => void) | null {
+ return this._onBufferEnded;
+ }
+
+ set onBufferEnded(callback: ((event: { bufferId: string }) => void) | null) {
+ this._onBufferEnded = callback;
+ }
+}
+
+class StreamerNodeMock extends AudioScheduledSourceNodeMock {
+ private hasBeenSetup: boolean = false;
+ private _streamPath: string = '';
+
+ constructor(context: BaseAudioContextMock, options?: TStreamerOptions) {
+ super(context, {});
+ if (options?.streamPath) {
+ this.initialize(options.streamPath);
+ }
+ }
+
+ initialize(streamPath: string): boolean {
+ if (this.hasBeenSetup) {
+ throw new Error('Node is already setup');
+ }
+ this._streamPath = streamPath;
+ this.hasBeenSetup = true;
+ return true;
+ }
+
+ get streamPath(): string {
+ return this._streamPath;
+ }
+
+ pause(): void {}
+ resume(): void {}
+}
+
+class WorkletNodeMock extends AudioNodeMock {
+ constructor(
+ context: BaseAudioContextMock,
+ _runtime: AudioWorkletRuntime,
+ _callback: (audioData: Array, channelCount: number) => void,
+ _bufferLength: number,
+ _inputChannelCount: number
+ ) {
+ super(context, {});
+ }
+}
+
+class WorkletProcessingNodeMock extends AudioNodeMock {
+ constructor(
+ context: BaseAudioContextMock,
+ _runtime: AudioWorkletRuntime,
+ _callback: (
+ inputData: Array,
+ outputData: Array,
+ framesToProcess: number,
+ currentTime: number
+ ) => void
+ ) {
+ super(context, {});
+ }
+}
+
+class WorkletSourceNodeMock extends AudioScheduledSourceNodeMock {
+ constructor(
+ context: BaseAudioContextMock,
+ _runtime: AudioWorkletRuntime,
+ _callback: (
+ audioData: Array,
+ framesToProcess: number,
+ currentTime: number,
+ startOffset: number
+ ) => void
+ ) {
+ super(context, {});
+ }
+}
+
+class PeriodicWaveMock {
+ constructor(
+ _context: BaseAudioContextMock,
+ _options?: TPeriodicWaveOptions
+ ) {}
+}
+
+class AudioDestinationNodeMock extends AudioNodeMock {
+ public maxChannelCount: number = 2;
+
+ constructor(context: BaseAudioContextMock) {
+ super(context, {});
+ this.numberOfOutputs = 0;
+ }
+}
+
+class BaseAudioContextMock {
+ public destination: AudioDestinationNodeMock;
+ private _sampleRate: number = 44100;
+ private _currentTime: number = 0;
+ protected _state: ContextState = 'running';
+
+ constructor(options?: AudioContextOptions) {
+ this.destination = new AudioDestinationNodeMock(this);
+ if (options?.sampleRate) {
+ this._sampleRate = options.sampleRate;
+ }
+ }
+
+ get currentTime(): number {
+ return this._currentTime;
+ }
+
+ get sampleRate(): number {
+ return this._sampleRate;
+ }
+
+ get state(): ContextState {
+ return this._state;
+ }
+
+ createBuffer(
+ numberOfChannels: number,
+ length: number,
+ sampleRate: number
+ ): AudioBufferMock {
+ return new AudioBufferMock({ numberOfChannels, length, sampleRate });
+ }
+
+ createPeriodicWave(
+ _real?: Float32Array,
+ _imag?: Float32Array,
+ _constraints?: { disableNormalization?: boolean }
+ ): PeriodicWaveMock {
+ return new PeriodicWaveMock(this);
+ }
+
+ decodeAudioData(_audioData: ArrayBuffer): Promise {
+ return Promise.resolve(
+ new AudioBufferMock({
+ numberOfChannels: 2,
+ length: 44100,
+ sampleRate: 44100,
+ })
+ );
+ }
+
+ createAnalyser(options?: TAnalyserOptions): AnalyserNodeMock {
+ return new AnalyserNodeMock(this, options);
+ }
+
+ createBiquadFilter(options?: TBiquadFilterOptions): BiquadFilterNodeMock {
+ return new BiquadFilterNodeMock(this, options);
+ }
+
+ createBufferSource(
+ options?: TAudioBufferSourceOptions
+ ): AudioBufferSourceNodeMock {
+ return new AudioBufferSourceNodeMock(this, options);
+ }
+
+ createChannelMerger(_numberOfInputs?: number): AudioNodeMock {
+ return new AudioNodeMock(this, {});
+ }
+
+ createChannelSplitter(_numberOfOutputs?: number): AudioNodeMock {
+ return new AudioNodeMock(this, {});
+ }
+
+ createConstantSource(
+ options?: TConstantSourceOptions
+ ): ConstantSourceNodeMock {
+ return new ConstantSourceNodeMock(this, options);
+ }
+
+ createConvolver(options?: TConvolverOptions): ConvolverNodeMock {
+ return new ConvolverNodeMock(this, options);
+ }
+
+ createDelay(options?: TDelayOptions): DelayNodeMock {
+ return new DelayNodeMock(this, options);
+ }
+
+ createGain(options?: TGainOptions): GainNodeMock {
+ return new GainNodeMock(this, options);
+ }
+
+ createOscillator(options?: TOscillatorOptions): OscillatorNodeMock {
+ return new OscillatorNodeMock(this, options);
+ }
+
+ createStereoPanner(options?: TStereoPannerOptions): StereoPannerNodeMock {
+ return new StereoPannerNodeMock(this, options);
+ }
+
+ createWaveShaper(options?: TWaveShaperOptions): WaveShaperNodeMock {
+ return new WaveShaperNodeMock(this, options);
+ }
+
+ createRecorderAdapter(): RecorderAdapterNodeMock {
+ return new RecorderAdapterNodeMock(this);
+ }
+
+ createBufferQueueSource(
+ options?: TBaseAudioBufferSourceOptions
+ ): AudioBufferQueueSourceNodeMock {
+ return new AudioBufferQueueSourceNodeMock(this, options);
+ }
+
+ createStreamer(options?: TStreamerOptions): StreamerNodeMock {
+ return new StreamerNodeMock(this, options);
+ }
+
+ createWorkletNode(
+ _shareableWorklet: Record,
+ _runOnUI: boolean,
+ _bufferLength: number,
+ _inputChannelCount: number
+ ): WorkletNodeMock {
+ return new WorkletNodeMock(this, 'AudioRuntime', noop, 0, 0);
+ }
+
+ createWorkletProcessingNode(
+ _shareableWorklet: Record,
+ _runOnUI: boolean
+ ): WorkletProcessingNodeMock {
+ return new WorkletProcessingNodeMock(this, 'AudioRuntime', noop);
+ }
+
+ createWorkletSourceNode(
+ _shareableWorklet: Record,
+ _runOnUI: boolean
+ ): WorkletSourceNodeMock {
+ return new WorkletSourceNodeMock(this, 'AudioRuntime', noop);
+ }
+}
+
+class AudioContextMock extends BaseAudioContextMock {
+ constructor(options?: AudioContextOptions) {
+ super(options);
+ }
+
+ close(): Promise {
+ this._state = 'closed';
+ return Promise.resolve();
+ }
+
+ resume(): Promise {
+ this._state = 'running';
+ return Promise.resolve();
+ }
+
+ suspend(): Promise {
+ this._state = 'suspended';
+ return Promise.resolve();
+ }
+}
+
+class OfflineAudioContextMock extends BaseAudioContextMock {
+ public length: number;
+
+ constructor(options: OfflineAudioContextOptions) {
+ super({ sampleRate: options.sampleRate });
+ this.length = options.length;
+ }
+
+ startRendering(): Promise {
+ return Promise.resolve(
+ new AudioBufferMock({
+ numberOfChannels: 2,
+ length: this.length,
+ sampleRate: this.sampleRate,
+ })
+ );
+ }
+}
+
+class AudioRecorderMock {
+ private _isRecording: boolean = false;
+ private _isPaused: boolean = false;
+ private _currentDuration: number = 0;
+ private _options: AudioRecorderFileOptions | null = null;
+ private isFileOutputEnabled: boolean = false;
+ private eventEmitter = new MockAudioEventEmitter();
+ private onAudioReadySubscription: MockEventSubscription | null = null;
+ private onErrorSubscription: MockEventSubscription | null = null;
+
+ constructor() {}
+
+ enableFileOutput(
+ options?: Omit
+ ): Result<{ path: string }> {
+ this._options = options || {};
+ this.isFileOutputEnabled = true;
+ return { status: 'success', path: '/mock/path/recordings' };
+ }
+
+ get options(): AudioRecorderFileOptions | null {
+ return this._options;
+ }
+
+ disableFileOutput(): void {
+ this._options = null;
+ this.isFileOutputEnabled = false;
+ }
+
+ start(options?: AudioRecorderStartOptions): Result<{ path: string }> {
+ this._isRecording = true;
+ this._isPaused = false;
+ const path = options?.fileNameOverride || 'recording.m4a';
+ return { status: 'success', path };
+ }
+
+ stop(): Result {
+ this._isRecording = false;
+ this._isPaused = false;
+ this._currentDuration = 0;
+ return {
+ status: 'success',
+ path: '/mock/path/recording.m4a',
+ size: 12345,
+ duration: 5.0,
+ };
+ }
+
+ pause(): void {
+ this._isPaused = true;
+ }
+
+ resume(): void {
+ this._isPaused = false;
+ }
+
+ connect(_node: RecorderAdapterNodeMock): void {
+ if (_node.wasConnected) {
+ throw new Error('RecorderAdapterNode cannot be connected more than once');
+ }
+ _node.wasConnected = true;
+ }
+
+ disconnect(): void {}
+
+ onAudioReady(
+ _options: AudioRecorderCallbackOptions,
+ callback: (event: Partial> | undefined) => void
+ ): Result<{}> {
+ if (this.onAudioReadySubscription) {
+ this.onAudioReadySubscription.remove();
+ }
+
+ this.onAudioReadySubscription = this.eventEmitter.addAudioEventListener(
+ 'audioReady',
+ callback
+ );
+
+ return { status: 'success' };
+ }
+
+ clearOnAudioReady(): void {
+ if (this.onAudioReadySubscription) {
+ this.onAudioReadySubscription.remove();
+ this.onAudioReadySubscription = null;
+ }
+ }
+
+ isRecording(): boolean {
+ return this._isRecording;
+ }
+
+ isPaused(): boolean {
+ return this._isPaused;
+ }
+
+ getCurrentDuration(): number {
+ return this._currentDuration;
+ }
+
+ onError(
+ callback: (error: Record | undefined) => void
+ ): void {
+ if (this.onErrorSubscription) {
+ this.onErrorSubscription.remove();
+ }
+
+ this.onErrorSubscription = this.eventEmitter.addAudioEventListener(
+ 'recorderError',
+ callback
+ );
+ }
+
+ clearOnError(): void {
+ if (this.onErrorSubscription) {
+ this.onErrorSubscription.remove();
+ this.onErrorSubscription = null;
+ }
+ }
+}
+
+const decodeAudioData = (_audioData: ArrayBuffer): Promise => {
+ return Promise.resolve(
+ new AudioBufferMock({
+ numberOfChannels: 2,
+ length: 44100,
+ sampleRate: 44100,
+ })
+ );
+};
+
+const decodePCMInBase64 = (_base64Data: string): Promise => {
+ return Promise.resolve(
+ new AudioBufferMock({
+ numberOfChannels: 2,
+ length: 44100,
+ sampleRate: 44100,
+ })
+ );
+};
+
+const changePlaybackSpeed = (
+ buffer: AudioBufferMock,
+ _speed: number
+): Promise => {
+ return Promise.resolve(buffer);
+};
+
+class AudioManagerMock {
+ static getDevicePreferredSampleRate(): number {
+ return 44100;
+ }
+
+ static observeVolumeChanges(_observe: boolean): void {}
+
+ static addSystemEventListener(
+ _event: string,
+ _callback: (event: { value: number }) => void
+ ): { remove: () => void } {
+ return { remove: noop };
+ }
+
+ static removeSystemEventListener(_listener: { remove: () => void }): void {}
+}
+
+class NotificationManagerMock {
+ static create(_options: Record): {
+ update: () => void;
+ destroy: () => void;
+ } {
+ return {
+ update: noop,
+ destroy: noop,
+ };
+ }
+}
+
+class PlaybackNotificationManagerMock {
+ static create(_options: Record): {
+ update: () => void;
+ destroy: () => void;
+ } {
+ return {
+ update: noop,
+ destroy: noop,
+ };
+ }
+}
+
+class RecordingNotificationManagerMock {
+ static create(_options: Record): {
+ update: () => void;
+ destroy: () => void;
+ } {
+ return {
+ update: noop,
+ destroy: noop,
+ };
+ }
+}
+
+let mockSystemVolumeValue = 0.5;
+const useSystemVolume = (): number => mockSystemVolumeValue;
+const setMockSystemVolume = (volume: number): void => {
+ mockSystemVolumeValue = volume;
+};
+
+class FilePresetMock {
+ static get Low(): FilePresetType {
+ return {} as FilePresetType;
+ }
+
+ static get Medium(): FilePresetType {
+ return {} as FilePresetType;
+ }
+
+ static get High(): FilePresetType {
+ return {} as FilePresetType;
+ }
+
+ static get Lossless(): FilePresetType {
+ return {} as FilePresetType;
+ }
+}
+
+class NotSupportedErrorMock extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = 'NotSupportedError';
+ }
+}
+
+class InvalidAccessErrorMock extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = 'InvalidAccessError';
+ }
+}
+
+class InvalidStateErrorMock extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = 'InvalidStateError';
+ }
+}
+
+class IndexSizeErrorMock extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = 'IndexSizeError';
+ }
+}
+
+class RangeErrorMock extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = 'RangeError';
+ }
+}
+
+class AudioApiErrorMock extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = 'AudioApiError';
+ }
+}
+
+// Export classes with original API names (for compatibility)
+export const AnalyserNode = AnalyserNodeMock;
+export const AudioBuffer = AudioBufferMock;
+export const AudioBufferQueueSourceNode = AudioBufferQueueSourceNodeMock;
+export const AudioBufferSourceNode = AudioBufferSourceNodeMock;
+export const AudioContext = AudioContextMock;
+export const AudioDestinationNode = AudioDestinationNodeMock;
+export const AudioNode = AudioNodeMock;
+export const AudioParam = AudioParamMock;
+export const AudioRecorder = AudioRecorderMock;
+export const AudioScheduledSourceNode = AudioScheduledSourceNodeMock;
+export const BaseAudioContext = BaseAudioContextMock;
+export const BiquadFilterNode = BiquadFilterNodeMock;
+export const ConstantSourceNode = ConstantSourceNodeMock;
+export const ConvolverNode = ConvolverNodeMock;
+export const DelayNode = DelayNodeMock;
+export const GainNode = GainNodeMock;
+export const OfflineAudioContext = OfflineAudioContextMock;
+export const OscillatorNode = OscillatorNodeMock;
+export const RecorderAdapterNode = RecorderAdapterNodeMock;
+export const StereoPannerNode = StereoPannerNodeMock;
+export const StreamerNode = StreamerNodeMock;
+export const WaveShaperNode = WaveShaperNodeMock;
+export const WorkletNode = WorkletNodeMock;
+export const WorkletProcessingNode = WorkletProcessingNodeMock;
+export const WorkletSourceNode = WorkletSourceNodeMock;
+export const PeriodicWave = PeriodicWaveMock;
+
+export const AudioManager = AudioManagerMock;
+export const NotificationManager = NotificationManagerMock;
+export const PlaybackNotificationManager = PlaybackNotificationManagerMock;
+export const RecordingNotificationManager = RecordingNotificationManagerMock;
+
+export const FilePreset = FilePresetMock;
+
+export const NotSupportedError = NotSupportedErrorMock;
+export const InvalidAccessError = InvalidAccessErrorMock;
+export const InvalidStateError = InvalidStateErrorMock;
+export const IndexSizeError = IndexSizeErrorMock;
+export const RangeError = RangeErrorMock;
+export const AudioApiError = AudioApiErrorMock;
+
+// Export functions
+export {
+ changePlaybackSpeed,
+ decodeAudioData,
+ decodePCMInBase64,
+ setMockSystemVolume,
+ useSystemVolume,
+};
+
+// Type exports to allow using classes as types
+export type AnalyserNode = AnalyserNodeMock;
+export type AudioBuffer = AudioBufferMock;
+export type AudioBufferQueueSourceNode = AudioBufferQueueSourceNodeMock;
+export type AudioBufferSourceNode = AudioBufferSourceNodeMock;
+export type AudioContext = AudioContextMock;
+export type AudioDestinationNode = AudioDestinationNodeMock;
+export type AudioNode = AudioNodeMock;
+export type AudioParam = AudioParamMock;
+export type AudioRecorder = AudioRecorderMock;
+export type AudioScheduledSourceNode = AudioScheduledSourceNodeMock;
+export type BaseAudioContext = BaseAudioContextMock;
+export type BiquadFilterNode = BiquadFilterNodeMock;
+export type ConstantSourceNode = ConstantSourceNodeMock;
+export type ConvolverNode = ConvolverNodeMock;
+export type DelayNode = DelayNodeMock;
+export type GainNode = GainNodeMock;
+export type OfflineAudioContext = OfflineAudioContextMock;
+export type OscillatorNode = OscillatorNodeMock;
+export type RecorderAdapterNode = RecorderAdapterNodeMock;
+export type StereoPannerNode = StereoPannerNodeMock;
+export type StreamerNode = StreamerNodeMock;
+export type WaveShaperNode = WaveShaperNodeMock;
+export type WorkletNode = WorkletNodeMock;
+export type WorkletProcessingNode = WorkletProcessingNodeMock;
+export type WorkletSourceNode = WorkletSourceNodeMock;
+export type PeriodicWave = PeriodicWaveMock;
+
+// Export types and enums
+export {
+ AudioContextOptions,
+ AudioRecorderCallbackOptions,
+ AudioRecorderFileOptions,
+ AudioRecorderStartOptions,
+ AudioWorkletRuntime,
+ BiquadFilterType,
+ ChannelCountMode,
+ ChannelInterpretation,
+ ContextState,
+ FileDirectory,
+ FileFormat,
+ FileInfo,
+ FilePresetType,
+ OfflineAudioContextOptions,
+ OscillatorType,
+ OverSampleType,
+ Result,
+ TAnalyserOptions,
+ TAudioBufferSourceOptions,
+ TBaseAudioBufferSourceOptions,
+ TBiquadFilterOptions,
+ TConstantSourceOptions,
+ TConvolverOptions,
+ TDelayOptions,
+ TGainOptions,
+ TOscillatorOptions,
+ TPeriodicWaveOptions,
+ TStereoPannerOptions,
+ TStreamerOptions,
+ TWaveShaperOptions,
+};
+
+export default {
+ AnalyserNode: AnalyserNodeMock,
+ AudioBuffer: AudioBufferMock,
+ AudioBufferQueueSourceNode: AudioBufferQueueSourceNodeMock,
+ AudioBufferSourceNode: AudioBufferSourceNodeMock,
+ AudioContext: AudioContextMock,
+ AudioDestinationNode: AudioDestinationNodeMock,
+ AudioNode: AudioNodeMock,
+ AudioParam: AudioParamMock,
+ AudioRecorder: AudioRecorderMock,
+ AudioScheduledSourceNode: AudioScheduledSourceNodeMock,
+ BaseAudioContext: BaseAudioContextMock,
+ BiquadFilterNode: BiquadFilterNodeMock,
+ ConstantSourceNode: ConstantSourceNodeMock,
+ ConvolverNode: ConvolverNodeMock,
+ DelayNode: DelayNodeMock,
+ GainNode: GainNodeMock,
+ OfflineAudioContext: OfflineAudioContextMock,
+ OscillatorNode: OscillatorNodeMock,
+ RecorderAdapterNode: RecorderAdapterNodeMock,
+ StereoPannerNode: StereoPannerNodeMock,
+ StreamerNode: StreamerNodeMock,
+ WaveShaperNode: WaveShaperNodeMock,
+ WorkletNode: WorkletNodeMock,
+ WorkletProcessingNode: WorkletProcessingNodeMock,
+ WorkletSourceNode: WorkletSourceNodeMock,
+ PeriodicWave: PeriodicWaveMock,
+
+ // Functions
+ decodeAudioData,
+ decodePCMInBase64,
+ changePlaybackSpeed,
+ useSystemVolume,
+ setMockSystemVolume,
+
+ // System classes
+ AudioManager: AudioManagerMock,
+ NotificationManager: NotificationManagerMock,
+ PlaybackNotificationManager: PlaybackNotificationManagerMock,
+ RecordingNotificationManager: RecordingNotificationManagerMock,
+
+ // Utils
+ FilePreset: FilePresetMock,
+
+ // Errors
+ NotSupportedError: NotSupportedErrorMock,
+ InvalidAccessError: InvalidAccessErrorMock,
+ InvalidStateError: InvalidStateErrorMock,
+ IndexSizeError: IndexSizeErrorMock,
+ RangeError: RangeErrorMock,
+ AudioApiError: AudioApiErrorMock,
+
+ // Enums
+ FileDirectory,
+ FileFormat,
+};
diff --git a/packages/react-native-audio-api/src/types.ts b/packages/react-native-audio-api/src/types.ts
index 01a6a8de8..d1c89c012 100644
--- a/packages/react-native-audio-api/src/types.ts
+++ b/packages/react-native-audio-api/src/types.ts
@@ -119,7 +119,7 @@ export interface TGainOptions extends TAudioNodeOptions {
gain?: number;
}
-export interface TSteroPannerOptions extends TAudioNodeOptions {
+export interface TStereoPannerOptions extends TAudioNodeOptions {
pan?: number;
}
diff --git a/packages/react-native-audio-api/src/web-core/StereoPannerNode.tsx b/packages/react-native-audio-api/src/web-core/StereoPannerNode.tsx
index 5c968c8ff..b163ff191 100644
--- a/packages/react-native-audio-api/src/web-core/StereoPannerNode.tsx
+++ b/packages/react-native-audio-api/src/web-core/StereoPannerNode.tsx
@@ -1,14 +1,14 @@
-import BaseAudioContext from './BaseAudioContext';
+import { TStereoPannerOptions } from '../types';
import AudioNode from './AudioNode';
import AudioParam from './AudioParam';
-import { TSteroPannerOptions } from '../types';
+import BaseAudioContext from './BaseAudioContext';
export default class StereoPannerNode extends AudioNode {
readonly pan: AudioParam;
constructor(
context: BaseAudioContext,
- stereoPannerOptions?: TSteroPannerOptions
+ stereoPannerOptions?: TStereoPannerOptions
) {
const pan = new globalThis.StereoPannerNode(
context.context,
diff --git a/packages/react-native-audio-api/tests/integration.test.ts b/packages/react-native-audio-api/tests/integration.test.ts
new file mode 100644
index 000000000..fbc2d19af
--- /dev/null
+++ b/packages/react-native-audio-api/tests/integration.test.ts
@@ -0,0 +1,359 @@
+/* eslint-disable */
+
+import * as MockAPI from '../src/mock';
+
+describe('Mock Integration Tests', () => {
+ describe('Audio Graph Creation', () => {
+ it('should create a complete audio graph with oscillator, gain, and destination', () => {
+ const context = new MockAPI.AudioContext();
+
+ // Create nodes
+ const oscillator = context.createOscillator();
+ const gainNode = context.createGain();
+
+ // Configure oscillator
+ oscillator.type = 'sine';
+ oscillator.frequency.value = 440;
+
+ // Configure gain
+ gainNode.gain.value = 0.5;
+
+ // Connect the graph
+ oscillator.connect(gainNode);
+ gainNode.connect(context.destination);
+
+ // Verify the setup
+ expect(oscillator.type).toBe('sine');
+ expect(oscillator.frequency.value).toBe(440);
+ expect(gainNode.gain.value).toBe(0.5);
+ expect(context.destination.numberOfOutputs).toBe(0);
+ });
+
+ it('should create an audio graph with effects chain', () => {
+ const context = new MockAPI.AudioContext();
+
+ const oscillator = context.createOscillator();
+ const filter = context.createBiquadFilter();
+ const delay = context.createDelay();
+ const gainNode = context.createGain();
+
+ // Configure effects
+ filter.type = 'lowpass';
+ filter.frequency.value = 2000;
+ delay.delayTime.value = 0.3;
+ gainNode.gain.value = 0.8;
+
+ // Create effects chain
+ oscillator.connect(filter);
+ filter.connect(delay);
+ delay.connect(gainNode);
+ gainNode.connect(context.destination);
+
+ expect(filter.type).toBe('lowpass');
+ expect(delay.delayTime.value).toBe(0.3);
+ });
+ });
+
+ describe('Audio Recording Workflow', () => {
+ it('should set up recording with adapter node', () => {
+ const context = new MockAPI.AudioContext();
+ const recorder = new MockAPI.AudioRecorder();
+
+ // Configure file output
+ const configResult = recorder.enableFileOutput({
+ format: MockAPI.FileFormat.M4A,
+ channelCount: 2,
+ directory: MockAPI.FileDirectory.Document,
+ });
+
+ expect(configResult.status).toBe('success');
+
+ // Create audio source
+ const oscillator = context.createOscillator();
+ const recorderAdapter = context.createRecorderAdapter();
+
+ // Connect to recorder
+ oscillator.connect(recorderAdapter);
+ recorder.connect(recorderAdapter);
+
+ // Start recording
+ const startResult = recorder.start({
+ fileNameOverride: 'test-recording.m4a',
+ });
+ expect(startResult.status).toBe('success');
+ expect(recorder.isRecording()).toBe(true);
+
+ // Stop recording
+ const stopResult = recorder.stop();
+ expect(stopResult.status).toBe('success');
+ expect((stopResult as { path?: string }).path).toBeDefined();
+ expect(recorder.isRecording()).toBe(false);
+ });
+
+ it('should handle audio data callbacks during recording', () => {
+ const recorder = new MockAPI.AudioRecorder();
+ const audioDataCallback = jest.fn();
+
+ const result = recorder.onAudioReady(
+ {
+ sampleRate: 44100,
+ bufferLength: 1024,
+ channelCount: 2,
+ },
+ audioDataCallback
+ );
+
+ expect(result.status).toBe('success');
+
+ // Simulate clearing callback
+ recorder.clearOnAudioReady();
+
+ // Should not throw when clearing again
+ expect(() => recorder.clearOnAudioReady()).not.toThrow();
+ });
+ });
+
+ describe('Offline Audio Processing', () => {
+ it('should render offline audio context', async () => {
+ const offlineContext = new MockAPI.OfflineAudioContext({
+ numberOfChannels: 2,
+ length: 44100, // 1 second at 44.1kHz
+ sampleRate: 44100,
+ });
+
+ // Create a simple tone
+ const oscillator = offlineContext.createOscillator();
+ oscillator.frequency.value = 440;
+ oscillator.connect(offlineContext.destination);
+
+ // Start rendering
+ const renderedBuffer = await offlineContext.startRendering();
+
+ expect(renderedBuffer).toBeInstanceOf(MockAPI.AudioBuffer);
+ expect(renderedBuffer.length).toBe(44100);
+ expect(renderedBuffer.sampleRate).toBe(44100);
+ expect(renderedBuffer.numberOfChannels).toBe(2);
+ });
+ });
+
+ describe('Worklet Processing', () => {
+ it('should create and use worklet nodes for custom processing', () => {
+ const context = new MockAPI.AudioContext();
+
+ const processingCallback = jest.fn(
+ (inputData, outputData, framesToProcess) => {
+ // Mock audio processing logic
+ for (let channel = 0; channel < outputData.length; channel++) {
+ for (let i = 0; i < framesToProcess; i++) {
+ outputData[channel][i] = inputData[channel][i] * 0.5; // Simple gain
+ }
+ }
+ }
+ );
+
+ const workletNode = new MockAPI.WorkletProcessingNode(
+ context,
+ 'AudioRuntime',
+ processingCallback
+ );
+
+ expect(workletNode).toBeInstanceOf(MockAPI.WorkletProcessingNode);
+ expect(workletNode.context).toBe(context);
+ });
+
+ it('should create worklet source node for custom audio generation', () => {
+ const context = new MockAPI.AudioContext();
+
+ const sourceCallback = jest.fn(
+ (audioData, framesToProcess, currentTime) => {
+ // Mock audio generation logic
+ for (let channel = 0; channel < audioData.length; channel++) {
+ for (let i = 0; i < framesToProcess; i++) {
+ audioData[channel][i] = Math.sin(currentTime + i); // Simple sine wave
+ }
+ }
+ }
+ );
+
+ const workletSource = new MockAPI.WorkletSourceNode(
+ context,
+ 'UIRuntime',
+ sourceCallback
+ );
+
+ expect(workletSource).toBeInstanceOf(MockAPI.WorkletSourceNode);
+ expect(workletSource.context).toBe(context);
+ });
+ });
+
+ describe('Buffer Queue Management', () => {
+ it('should manage audio buffer queue for seamless playback', () => {
+ const context = new MockAPI.AudioContext();
+ const queueSource = context.createBufferQueueSource();
+
+ // Create multiple buffers
+ const buffer1 = context.createBuffer(2, 1024, 44100);
+ const buffer2 = context.createBuffer(2, 1024, 44100);
+ const buffer3 = context.createBuffer(2, 1024, 44100);
+
+ // Enqueue buffers
+ const id1 = queueSource.enqueueBuffer(buffer1);
+ const id2 = queueSource.enqueueBuffer(buffer2);
+ const id3 = queueSource.enqueueBuffer(buffer3);
+
+ expect(typeof id1).toBe('string');
+ expect(typeof id2).toBe('string');
+ expect(typeof id3).toBe('string');
+ expect(id1).not.toBe(id2);
+
+ // Set up buffer ended callback
+ const bufferEndedCallback = jest.fn();
+ queueSource.onBufferEnded = bufferEndedCallback;
+
+ // Dequeue specific buffer
+ queueSource.dequeueBuffer(id2);
+
+ // Clear all buffers
+ queueSource.clearBuffers();
+
+ // Connect and start playback
+ queueSource.connect(context.destination);
+ queueSource.start();
+ });
+ });
+
+ describe('Streaming Audio', () => {
+ it('should set up audio streaming node', () => {
+ const context = new MockAPI.AudioContext();
+
+ // Create streamer with options
+ const streamer = context.createStreamer({
+ streamPath: 'https://example.com/audio-stream',
+ });
+
+ expect(streamer.streamPath).toBe('https://example.com/audio-stream');
+
+ // Connect to output
+ streamer.connect(context.destination);
+
+ // Control playback
+ streamer.start();
+ streamer.pause();
+ streamer.resume();
+ streamer.stop();
+ });
+
+ it('should handle streamer initialization errors', () => {
+ const context = new MockAPI.AudioContext();
+ const streamer = context.createStreamer();
+
+ // First initialization should succeed
+ expect(streamer.initialize('http://stream1.com')).toBe(true);
+
+ // Second initialization should fail
+ expect(() => streamer.initialize('http://stream2.com')).toThrow();
+ });
+ });
+
+ describe('System Integration', () => {
+ it('should integrate with system audio management', () => {
+ // Get preferred sample rate
+ const preferredRate = MockAPI.AudioManager.getDevicePreferredSampleRate();
+ expect(preferredRate).toBe(44100);
+
+ // Set up volume monitoring
+ const volumeCallback = jest.fn();
+ const listener = MockAPI.AudioManager.addSystemEventListener(
+ 'volumeChange',
+ volumeCallback
+ );
+
+ MockAPI.AudioManager.observeVolumeChanges(true);
+
+ // Simulate volume change
+ MockAPI.setMockSystemVolume(0.7);
+ const currentVolume = MockAPI.useSystemVolume();
+ expect(currentVolume).toBe(0.7);
+
+ // Clean up
+ MockAPI.AudioManager.observeVolumeChanges(false);
+ listener.remove();
+ });
+
+ it('should create and manage notifications', () => {
+ const playbackNotification = MockAPI.PlaybackNotificationManager.create({
+ title: 'Test Song',
+ artist: 'Test Artist',
+ artwork: 'https://example.com/artwork.jpg',
+ });
+
+ expect(playbackNotification.update).toBeDefined();
+ expect(playbackNotification.destroy).toBeDefined();
+
+ // Update notification
+ playbackNotification.update();
+
+ // Clean up
+ playbackNotification.destroy();
+ });
+ });
+
+ describe('Error Handling', () => {
+ it('should handle various error conditions', () => {
+ // Test NotSupportedError
+ expect(() => {
+ throw new MockAPI.NotSupportedError('Feature not supported');
+ }).toThrow('Feature not supported');
+
+ // Test InvalidStateError
+ expect(() => {
+ throw new MockAPI.InvalidStateError('Invalid state');
+ }).toThrow('Invalid state');
+
+ // Test recorder connection errors
+ const recorder = new MockAPI.AudioRecorder();
+ const context = new MockAPI.AudioContext();
+ const adapter = context.createRecorderAdapter();
+
+ // First connection should work
+ recorder.connect(adapter);
+
+ // Second connection should throw
+ expect(() => recorder.connect(adapter)).toThrow();
+ });
+ });
+
+ describe('File Presets and Configuration', () => {
+ it('should use different quality presets', () => {
+ const recorder = new MockAPI.AudioRecorder();
+
+ // Test with different presets
+ const lowQualityConfig = {
+ preset: MockAPI.FilePreset.Low,
+ format: MockAPI.FileFormat.M4A,
+ };
+
+ const highQualityConfig = {
+ preset: MockAPI.FilePreset.High,
+ format: MockAPI.FileFormat.Wav,
+ };
+
+ const losslessConfig = {
+ preset: MockAPI.FilePreset.Lossless,
+ format: MockAPI.FileFormat.Wav,
+ };
+
+ // Configure with different presets
+ recorder.enableFileOutput(lowQualityConfig);
+ expect(recorder.options?.format).toBe(MockAPI.FileFormat.M4A);
+
+ recorder.disableFileOutput();
+ recorder.enableFileOutput(highQualityConfig);
+ expect(recorder.options?.format).toBe(MockAPI.FileFormat.Wav);
+
+ recorder.disableFileOutput();
+ recorder.enableFileOutput(losslessConfig);
+ expect(recorder.options?.format).toBe(MockAPI.FileFormat.Wav);
+ });
+ });
+});
diff --git a/packages/react-native-audio-api/tests/jest.config.json b/packages/react-native-audio-api/tests/jest.config.json
new file mode 100644
index 000000000..46f9c8dab
--- /dev/null
+++ b/packages/react-native-audio-api/tests/jest.config.json
@@ -0,0 +1,9 @@
+{
+ "preset": "react-native",
+ "rootDir": "..",
+ "testMatch": ["**/tests/**/*.test.ts", "**/tests/**/*.test.tsx"],
+ "collectCoverageFrom": ["src/mock/**/*.{ts,tsx}", "!src/mock/**/*.d.ts"],
+ "coverageDirectory": "tests/coverage",
+ "coverageReporters": ["text", "lcov", "html"],
+ "setupFilesAfterEnv": ["/tests/setup.ts"]
+}
diff --git a/packages/react-native-audio-api/tests/mock.test.ts b/packages/react-native-audio-api/tests/mock.test.ts
new file mode 100644
index 000000000..a5a79919a
--- /dev/null
+++ b/packages/react-native-audio-api/tests/mock.test.ts
@@ -0,0 +1,512 @@
+/* eslint-disable */
+
+import * as MockAPI from '../src/mock';
+
+describe('React Native Audio API Mocks', () => {
+ describe('AudioContext', () => {
+ it('should create an AudioContext with default sample rate', () => {
+ const context = new MockAPI.AudioContext();
+ expect(context.sampleRate).toBe(44100);
+ expect(context.currentTime).toBe(0);
+ expect(context.state).toBe('running');
+ expect(context.destination).toBeInstanceOf(MockAPI.AudioDestinationNode);
+ });
+
+ it('should create an AudioContext with custom sample rate', () => {
+ const context = new MockAPI.AudioContext({ sampleRate: 48000 });
+ expect(context.sampleRate).toBe(48000);
+ });
+
+ it('should support context state management', async () => {
+ const context = new MockAPI.AudioContext();
+ expect(context.state).toBe('running');
+
+ await context.suspend();
+ expect(context.state).toBe('suspended');
+
+ await context.resume();
+ expect(context.state).toBe('running');
+
+ await context.close();
+ expect(context.state).toBe('closed');
+ });
+ });
+
+ describe('OfflineAudioContext', () => {
+ it('should create an OfflineAudioContext with specified parameters', () => {
+ const context = new MockAPI.OfflineAudioContext({
+ numberOfChannels: 2,
+ length: 44100,
+ sampleRate: 44100,
+ });
+
+ expect(context.sampleRate).toBe(44100);
+ expect(context.length).toBe(44100);
+ });
+
+ it('should start rendering and return an AudioBuffer', async () => {
+ const context = new MockAPI.OfflineAudioContext({
+ numberOfChannels: 2,
+ length: 44100,
+ sampleRate: 44100,
+ });
+
+ const buffer = await context.startRendering();
+ expect(buffer).toBeInstanceOf(MockAPI.AudioBuffer);
+ expect(buffer.numberOfChannels).toBe(2);
+ expect(buffer.length).toBe(44100);
+ expect(buffer.sampleRate).toBe(44100);
+ });
+ });
+
+ describe('AudioBuffer', () => {
+ it('should create an AudioBuffer with correct properties', () => {
+ const buffer = new MockAPI.AudioBuffer({
+ numberOfChannels: 2,
+ length: 1024,
+ sampleRate: 44100,
+ });
+
+ expect(buffer.numberOfChannels).toBe(2);
+ expect(buffer.length).toBe(1024);
+ expect(buffer.sampleRate).toBe(44100);
+ expect(buffer.duration).toBeCloseTo(1024 / 44100);
+ });
+
+ it('should provide channel data', () => {
+ const buffer = new MockAPI.AudioBuffer({
+ numberOfChannels: 2,
+ length: 1024,
+ sampleRate: 44100,
+ });
+
+ const channelData = buffer.getChannelData(0);
+ expect(channelData).toBeInstanceOf(Float32Array);
+ expect(channelData.length).toBe(1024);
+ });
+ });
+
+ describe('Audio Nodes', () => {
+ let context: MockAPI.AudioContext;
+
+ beforeEach(() => {
+ context = new MockAPI.AudioContext();
+ });
+
+ describe('GainNode', () => {
+ it('should create a GainNode with correct properties', () => {
+ const gainNode = context.createGain();
+ expect(gainNode).toBeInstanceOf(MockAPI.GainNode);
+ expect(gainNode.gain).toBeInstanceOf(MockAPI.AudioParam);
+ expect(gainNode.gain.value).toBe(1);
+ });
+
+ it('should allow gain parameter manipulation', () => {
+ const gainNode = context.createGain();
+ gainNode.gain.value = 0.5;
+ expect(gainNode.gain.value).toBe(0.5);
+
+ const result = gainNode.gain.setValueAtTime(0.8, 0);
+ expect(result).toBe(gainNode.gain);
+ expect(gainNode.gain.value).toBe(0.8);
+ });
+ });
+
+ describe('OscillatorNode', () => {
+ it('should create an OscillatorNode with correct properties', () => {
+ const oscillator = context.createOscillator();
+ expect(oscillator).toBeInstanceOf(MockAPI.OscillatorNode);
+ expect(oscillator.frequency).toBeInstanceOf(MockAPI.AudioParam);
+ expect(oscillator.frequency.value).toBe(440);
+ expect(oscillator.type).toBe('sine');
+ });
+
+ it('should allow type changes', () => {
+ const oscillator = context.createOscillator();
+ oscillator.type = 'square';
+ expect(oscillator.type).toBe('square');
+ });
+
+ it('should support start/stop methods', () => {
+ const oscillator = context.createOscillator();
+ expect(() => oscillator.start()).not.toThrow();
+ expect(() => oscillator.stop()).not.toThrow();
+ });
+
+ it('should support onended callback', () => {
+ const oscillator = context.createOscillator();
+ const callback = jest.fn();
+ oscillator.onended = callback;
+ expect(oscillator.onended).toBe(callback);
+ });
+ });
+
+ describe('AnalyserNode', () => {
+ it('should create an AnalyserNode with correct properties', () => {
+ const analyser = context.createAnalyser();
+ expect(analyser).toBeInstanceOf(MockAPI.AnalyserNode);
+ expect(analyser.fftSize).toBe(2048);
+ expect(analyser.frequencyBinCount).toBe(1024);
+ });
+ });
+
+ describe('BiquadFilterNode', () => {
+ it('should create a BiquadFilterNode with correct properties', () => {
+ const filter = context.createBiquadFilter();
+ expect(filter).toBeInstanceOf(MockAPI.BiquadFilterNode);
+ expect(filter.type).toBe('lowpass');
+ expect(filter.frequency.value).toBe(350);
+ expect(filter.Q.value).toBe(1);
+ });
+ });
+ });
+
+ describe('Custom Nodes', () => {
+ let context: MockAPI.AudioContext;
+
+ beforeEach(() => {
+ context = new MockAPI.AudioContext();
+ });
+
+ describe('RecorderAdapterNode', () => {
+ it('should create a RecorderAdapterNode', () => {
+ const recorderAdapter = context.createRecorderAdapter();
+ expect(recorderAdapter).toBeInstanceOf(MockAPI.RecorderAdapterNode);
+ expect(recorderAdapter.wasConnected).toBe(false);
+ });
+ });
+
+ describe('AudioBufferQueueSourceNode', () => {
+ it('should create an AudioBufferQueueSourceNode', () => {
+ const queueSource = context.createBufferQueueSource();
+ expect(queueSource).toBeInstanceOf(MockAPI.AudioBufferQueueSourceNode);
+ });
+
+ it('should support buffer queueing operations', () => {
+ const queueSource = context.createBufferQueueSource();
+ const buffer = context.createBuffer(2, 1024, 44100);
+
+ const bufferId = queueSource.enqueueBuffer(buffer);
+ expect(typeof bufferId).toBe('string');
+ expect(bufferId.length).toBeGreaterThan(0);
+
+ expect(() => queueSource.dequeueBuffer(bufferId)).not.toThrow();
+ expect(() => queueSource.clearBuffers()).not.toThrow();
+ });
+
+ it('should support onBufferEnded callback', () => {
+ const queueSource = context.createBufferQueueSource();
+ const callback = jest.fn();
+ queueSource.onBufferEnded = callback;
+ expect(queueSource.onBufferEnded).toBe(callback);
+ });
+ });
+
+ describe('StreamerNode', () => {
+ it('should create a StreamerNode', () => {
+ const streamer = context.createStreamer();
+ expect(streamer).toBeInstanceOf(MockAPI.StreamerNode);
+ });
+
+ it('should support initialization with stream path', () => {
+ const streamer = context.createStreamer();
+ const result = streamer.initialize('http://example.com/stream');
+ expect(result).toBe(true);
+ expect(streamer.streamPath).toBe('http://example.com/stream');
+ });
+
+ it('should throw error on duplicate initialization', () => {
+ const streamer = context.createStreamer();
+ streamer.initialize('http://example.com/stream');
+ expect(() => streamer.initialize('http://example.com/other')).toThrow();
+ });
+ });
+
+ describe('WorkletNodes', () => {
+ it('should create WorkletNode', () => {
+ const workletNode = new MockAPI.WorkletNode(
+ context,
+ 'AudioRuntime',
+ () => {},
+ 1024,
+ 2
+ );
+ expect(workletNode).toBeInstanceOf(MockAPI.WorkletNode);
+ });
+
+ it('should create WorkletProcessingNode', () => {
+ const processingNode = new MockAPI.WorkletProcessingNode(
+ context,
+ 'AudioRuntime',
+ () => {}
+ );
+ expect(processingNode).toBeInstanceOf(MockAPI.WorkletProcessingNode);
+ });
+
+ it('should create WorkletSourceNode', () => {
+ const sourceNode = new MockAPI.WorkletSourceNode(
+ context,
+ 'AudioRuntime',
+ () => {}
+ );
+ expect(sourceNode).toBeInstanceOf(MockAPI.WorkletSourceNode);
+ });
+ });
+ });
+
+ describe('AudioRecorder', () => {
+ let recorder: MockAPI.AudioRecorder;
+
+ beforeEach(() => {
+ recorder = new MockAPI.AudioRecorder();
+ });
+
+ it('should create an AudioRecorder with correct initial state', () => {
+ expect(recorder.isRecording()).toBe(false);
+ expect(recorder.isPaused()).toBe(false);
+ expect(recorder.getCurrentDuration()).toBe(0);
+ expect(recorder.options).toBeNull();
+ });
+
+ it('should support file output configuration', () => {
+ const result = recorder.enableFileOutput({
+ format: MockAPI.FileFormat.M4A,
+ channelCount: 2,
+ });
+
+ expect(result.status).toBe('success');
+ expect((result as { path?: string }).path).toBeDefined();
+ expect(recorder.options).toBeDefined();
+ });
+
+ it('should support recording workflow', () => {
+ recorder.enableFileOutput();
+
+ const startResult = recorder.start();
+ expect(startResult.status).toBe('success');
+ expect(recorder.isRecording()).toBe(true);
+
+ recorder.pause();
+ expect(recorder.isPaused()).toBe(true);
+
+ recorder.resume();
+ expect(recorder.isPaused()).toBe(false);
+
+ const stopResult = recorder.stop();
+ expect(stopResult.status).toBe('success');
+ expect((stopResult as { path?: string }).path).toBeDefined();
+ expect(recorder.isRecording()).toBe(false);
+ });
+
+ it('should support RecorderAdapterNode connection', () => {
+ const context = new MockAPI.AudioContext();
+ const adapter = context.createRecorderAdapter();
+
+ expect(() => recorder.connect(adapter)).not.toThrow();
+ expect(adapter.wasConnected).toBe(true);
+
+ // Should throw on duplicate connection
+ expect(() => recorder.connect(adapter)).toThrow();
+ });
+
+ it('should support audio data callbacks', () => {
+ const callback = jest.fn();
+ const result = recorder.onAudioReady(
+ { sampleRate: 44100, bufferLength: 1024, channelCount: 2 },
+ callback
+ );
+
+ expect(result.status).toBe('success');
+
+ recorder.clearOnAudioReady();
+ // Should not throw
+ });
+
+ it('should support error callbacks', () => {
+ const callback = jest.fn();
+ expect(() => recorder.onError(callback)).not.toThrow();
+ expect(() => recorder.clearOnError()).not.toThrow();
+ });
+ });
+
+ describe('Utility Functions', () => {
+ it('should decode audio data', async () => {
+ const arrayBuffer = new ArrayBuffer(1024);
+ const buffer = await MockAPI.decodeAudioData(arrayBuffer);
+
+ expect(buffer).toBeInstanceOf(MockAPI.AudioBuffer);
+ expect(buffer.numberOfChannels).toBe(2);
+ expect(buffer.sampleRate).toBe(44100);
+ });
+
+ it('should decode PCM in base64', async () => {
+ const base64Data = 'SGVsbG8gV29ybGQ=';
+ const buffer = await MockAPI.decodePCMInBase64(base64Data);
+
+ expect(buffer).toBeInstanceOf(MockAPI.AudioBuffer);
+ });
+
+ it('should change playback speed', async () => {
+ const context = new MockAPI.AudioContext();
+ const inputBuffer = context.createBuffer(2, 1024, 44100);
+ const outputBuffer = await MockAPI.changePlaybackSpeed(inputBuffer, 1.5);
+
+ expect(outputBuffer).toBe(inputBuffer);
+ });
+ });
+
+ describe('System Classes', () => {
+ describe('AudioManager', () => {
+ it('should provide device preferred sample rate', () => {
+ const sampleRate = MockAPI.AudioManager.getDevicePreferredSampleRate();
+ expect(sampleRate).toBe(44100);
+ });
+
+ it('should support volume change observation', () => {
+ expect(() =>
+ MockAPI.AudioManager.observeVolumeChanges(true)
+ ).not.toThrow();
+ expect(() =>
+ MockAPI.AudioManager.observeVolumeChanges(false)
+ ).not.toThrow();
+ });
+
+ it('should support system event listeners', () => {
+ const callback = jest.fn();
+ const listener = MockAPI.AudioManager.addSystemEventListener(
+ 'volumeChange',
+ callback
+ );
+
+ expect(listener).toHaveProperty('remove');
+ expect(typeof listener.remove).toBe('function');
+
+ expect(() =>
+ MockAPI.AudioManager.removeSystemEventListener(listener)
+ ).not.toThrow();
+ });
+ });
+
+ describe('Notification Managers', () => {
+ it('should create notification managers', () => {
+ const options = { title: 'Test', artist: 'Test Artist' };
+
+ const notification = MockAPI.NotificationManager.create(options);
+ expect(notification).toHaveProperty('update');
+ expect(notification).toHaveProperty('destroy');
+
+ const playback = MockAPI.PlaybackNotificationManager.create(options);
+ expect(playback).toHaveProperty('update');
+ expect(playback).toHaveProperty('destroy');
+
+ const recording = MockAPI.RecordingNotificationManager.create(options);
+ expect(recording).toHaveProperty('update');
+ expect(recording).toHaveProperty('destroy');
+ });
+ });
+ });
+
+ describe('Hooks', () => {
+ it('should provide useSystemVolume hook', () => {
+ const volume = MockAPI.useSystemVolume();
+ expect(typeof volume).toBe('number');
+ expect(volume).toBe(0.5);
+
+ MockAPI.setMockSystemVolume(0.8);
+ const newVolume = MockAPI.useSystemVolume();
+ expect(newVolume).toBe(0.8);
+ });
+ });
+
+ describe('File Presets', () => {
+ it('should provide file preset configurations', () => {
+ expect(MockAPI.FilePreset.Low).toBeDefined();
+ expect(MockAPI.FilePreset.Medium).toBeDefined();
+ expect(MockAPI.FilePreset.High).toBeDefined();
+ expect(MockAPI.FilePreset.Lossless).toBeDefined();
+
+ expect(typeof MockAPI.FilePreset.Low).toBe('object');
+ expect(typeof MockAPI.FilePreset.Medium).toBe('object');
+ expect(typeof MockAPI.FilePreset.High).toBe('object');
+ expect(typeof MockAPI.FilePreset.Lossless).toBe('object');
+ });
+ });
+
+ describe('Error Classes', () => {
+ it('should provide custom error classes', () => {
+ const notSupportedError = new MockAPI.NotSupportedError('Not supported');
+ expect(notSupportedError).toBeInstanceOf(Error);
+ expect(notSupportedError.name).toBe('NotSupportedError');
+
+ const invalidAccessError = new MockAPI.InvalidAccessError(
+ 'Invalid access'
+ );
+ expect(invalidAccessError).toBeInstanceOf(Error);
+ expect(invalidAccessError.name).toBe('InvalidAccessError');
+
+ const invalidStateError = new MockAPI.InvalidStateError('Invalid state');
+ expect(invalidStateError).toBeInstanceOf(Error);
+ expect(invalidStateError.name).toBe('InvalidStateError');
+
+ const indexSizeError = new MockAPI.IndexSizeError('Index size error');
+ expect(indexSizeError).toBeInstanceOf(Error);
+ expect(indexSizeError.name).toBe('IndexSizeError');
+
+ const rangeError = new MockAPI.RangeError('Range error');
+ expect(rangeError).toBeInstanceOf(Error);
+ expect(rangeError.name).toBe('RangeError');
+
+ const audioApiError = new MockAPI.AudioApiError('Audio API error');
+ expect(audioApiError).toBeInstanceOf(Error);
+ expect(audioApiError.name).toBe('AudioApiError');
+ });
+ });
+
+ describe('Constants', () => {
+ it('should export FileDirectory constants', () => {
+ expect(MockAPI.FileDirectory.Document).toBe(0);
+ expect(MockAPI.FileDirectory.Cache).toBe(1);
+ });
+
+ it('should export FileFormat constants', () => {
+ expect(MockAPI.FileFormat.Wav).toBe(0);
+ expect(MockAPI.FileFormat.Caf).toBe(1);
+ });
+ });
+
+ describe('Node Connections', () => {
+ let context: MockAPI.AudioContext;
+
+ beforeEach(() => {
+ context = new MockAPI.AudioContext();
+ });
+
+ it('should support audio node connections', () => {
+ const oscillator = context.createOscillator();
+ const gainNode = context.createGain();
+
+ const result = oscillator.connect(gainNode);
+ expect(result).toBe(gainNode);
+
+ const result2 = gainNode.connect(context.destination);
+ expect(result2).toBe(context.destination);
+ });
+
+ it('should support AudioParam connections', () => {
+ const oscillator = context.createOscillator();
+ const gainNode = context.createGain();
+
+ const result = oscillator.connect(gainNode.gain);
+ expect(result).toBeUndefined();
+ });
+
+ it('should support disconnections', () => {
+ const oscillator = context.createOscillator();
+ const gainNode = context.createGain();
+
+ oscillator.connect(gainNode);
+ expect(() => oscillator.disconnect()).not.toThrow();
+ expect(() => oscillator.disconnect(gainNode)).not.toThrow();
+ });
+ });
+});
diff --git a/packages/react-native-audio-api/tests/setup.ts b/packages/react-native-audio-api/tests/setup.ts
new file mode 100644
index 000000000..e382f5148
--- /dev/null
+++ b/packages/react-native-audio-api/tests/setup.ts
@@ -0,0 +1,20 @@
+// __tests__/setup.ts
+import type { IAudioEventEmitter } from '../src/interfaces';
+
+// Mock global objects that might be needed
+global.createAudioContext = jest.fn();
+global.createAudioRecorder = jest.fn();
+global.AudioEventEmitter = {} as IAudioEventEmitter;
+
+// Set up global test environment
+beforeAll(() => {
+ // Suppress console warnings for tests
+ jest.spyOn(console, 'warn').mockImplementation(() => {});
+ jest.spyOn(console, 'error').mockImplementation(() => {});
+});
+
+afterAll(() => {
+ // Restore console methods
+ (console.warn as jest.Mock).mockRestore();
+ (console.error as jest.Mock).mockRestore();
+});