Skip to content
Open
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
107 changes: 107 additions & 0 deletions apps/common-app/src/examples/Distorted/Distorted.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import React, { useCallback, useEffect, useState, FC } from 'react';
import { ActivityIndicator } from 'react-native';
import {
AudioContext,
AudioNode,
AudioBuffer,
AudioBufferSourceNode,
} from 'react-native-audio-api';
import { Container, Button } from '../../components';
import { presetEffects } from '../../utils/effects';

// const URL =
// 'http://localhost:3000/react-native-audio-api/audio/music/guitar-sample.flac';

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

const Distorted: FC = () => {
const [isPlaying, setIsPlaying] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [buffer, setBuffer] = useState<AudioBuffer | null>(null);

const aCtxRef = React.useRef<AudioContext | null>(null);
const effectsMap = React.useRef<Map<string, AudioNode> | null>(null);
const sourceNodeRef = React.useRef<AudioBufferSourceNode | null>(null);

const fetchAudioBuffer = useCallback(async () => {
setIsLoading(true);

if (!aCtxRef.current) {
aCtxRef.current = new AudioContext();
}
const audioContext = aCtxRef.current;

effectsMap.current = presetEffects.distorted(audioContext);

const audioBuffer = await fetch(URL, {
headers: {
'User-Agent':
'Mozilla/5.0 (Android; Mobile; rv:122.0) Gecko/122.0 Firefox/122.0',
},
})
.then((response) => response.arrayBuffer())
.then((arrayBuffer) => audioContext.decodeAudioData(arrayBuffer))
.catch((error) => {
console.error('Error decoding audio data source:', error);
return null;
});

setBuffer(audioBuffer);

setIsLoading(false);
}, []);

const togglePlayPause = useCallback(async () => {
if (!aCtxRef.current) {
return;
}

if (buffer === null) {
fetchAudioBuffer();
return;
}

if (isPlaying) {
sourceNodeRef.current?.stop();
} else {
await aCtxRef.current.resume();
sourceNodeRef.current = aCtxRef.current.createBufferSource();
sourceNodeRef.current.buffer = buffer;

let previousNode: AudioNode = sourceNodeRef.current;
effectsMap.current?.forEach((node) => {
previousNode.connect(node);
previousNode = node;
});

previousNode.connect(aCtxRef.current.destination);

sourceNodeRef.current.start();
}

setIsPlaying((prev) => !prev);
}, [isPlaying, buffer, fetchAudioBuffer]);

useEffect(() => {
fetchAudioBuffer();

return () => {
aCtxRef.current?.close();
aCtxRef.current = null;
};
}, [fetchAudioBuffer]);

return (
<Container centered>
{isLoading && <ActivityIndicator color="#FFFFFF" />}
<Button
title={isPlaying ? 'Stop' : 'Play'}
onPress={togglePlayPause}
disabled={isLoading}
/>
</Container>
);
};

export default Distorted;
1 change: 1 addition & 0 deletions apps/common-app/src/examples/Distorted/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './Distorted';
8 changes: 8 additions & 0 deletions apps/common-app/src/examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Record from './Record/Record';
import PlaybackSpeed from './PlaybackSpeed/PlaybackSpeed';
import Worklets from './Worklets/Worklets';
import Streaming from './Streaming/Streaming';
import Distorted from './Distorted/Distorted';

type NavigationParamList = {
Oscillator: undefined;
Expand All @@ -25,6 +26,7 @@ type NavigationParamList = {
Record: undefined;
Worklets: undefined;
Streamer: undefined;
Distorted: undefined;
};

export type ExampleKey = keyof NavigationParamList;
Expand Down Expand Up @@ -104,4 +106,10 @@ export const Examples: Example[] = [
subtitle: 'Stream audio from a URL',
screen: Streaming,
},
{
key: 'Distorted',
title: 'Distorted Audio',
subtitle: 'Apply non-linear distortion effect to audio',
screen: Distorted,
},
] as const;
157 changes: 157 additions & 0 deletions apps/common-app/src/utils/effects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import {
AudioContext,
GainNode,
BiquadFilterNode,
ConvolverNode,
WaveShaperNode,
AudioNode,
} from 'react-native-audio-api';

export function createGainEffect(
audioContext: AudioContext,
gain: number = 1.0
): GainNode {
const gainNode = audioContext.createGain();
gainNode.gain.setValueAtTime(gain, audioContext.currentTime);
return gainNode;
}

export function createLowPassFilter(
audioContext: AudioContext,
frequency: number = 1000
): BiquadFilterNode {
const filter = audioContext.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(frequency, audioContext.currentTime);
filter.Q.setValueAtTime(1, audioContext.currentTime);
return filter;
}

export function createHighPassFilter(
audioContext: AudioContext,
frequency: number = 300
): BiquadFilterNode {
const filter = audioContext.createBiquadFilter();
filter.type = 'highpass';
filter.frequency.setValueAtTime(frequency, audioContext.currentTime);
filter.Q.setValueAtTime(1, audioContext.currentTime);
return filter;
}

export function createSimpleReverb(
audioContext: AudioContext,
roomSize: number = 0.5,
decayTime: number = 2
): ConvolverNode {
const convolver = audioContext.createConvolver();

const sampleRate = audioContext.sampleRate;
const length = sampleRate * decayTime;
const impulse = audioContext.createBuffer(2, length, sampleRate);

for (let channel = 0; channel < 2; channel++) {
const channelData = impulse.getChannelData(channel);
for (let i = 0; i < length; i++) {
const decay = Math.pow(1 - i / length, 2);
channelData[i] = (Math.random() * 2 - 1) * decay * roomSize;
}
}

convolver.buffer = impulse;
return convolver;
}

export function createOverdrive(
audioContext: AudioContext,
amount: number = 0.5
): WaveShaperNode {
const waveShaper = audioContext.createWaveShaper();

const samples = 44100;
const curve = new Float32Array(samples);

const clampedAmount = Math.max(0, Math.min(1, amount));
const drive = 2 + clampedAmount * 30;

for (let i = 0; i < samples; i++) {
const x = (i * 2) / samples - 1;
const driven = x * drive;

let distorted;
if (driven > 0) {
distorted = Math.tanh(driven * 1.5) * 0.8;
} else {
distorted = Math.tanh(driven * 1.2) * 0.9;
}

const harmonics = Math.sin(driven * 3) * 0.1 * clampedAmount;

curve[i] = Math.max(-1, Math.min(1, distorted + harmonics)) * 3;
}

waveShaper.curve = curve;
waveShaper.oversample = '4x';

return waveShaper;
}

export function createBandPassFilter(
audioContext: AudioContext,
lowFreq: number = 800,
highFreq: number = 3000
): BiquadFilterNode {
const filter = audioContext.createBiquadFilter();
filter.type = 'bandpass';

const centerFreq = Math.sqrt(lowFreq * highFreq);
filter.frequency.setValueAtTime(centerFreq, audioContext.currentTime);

const Q = centerFreq / (highFreq - lowFreq);
filter.Q.setValueAtTime(Q, audioContext.currentTime);

return filter;
}

export function createEffectsMap(
effects: { name: string; node: AudioNode }[]
): Map<string, AudioNode> {
const effectsMap = new Map<string, AudioNode>();

effects.forEach(({ name, node }) => {
effectsMap.set(name, node);
});

return effectsMap;
}

export const presetEffects = {
ambient: (audioContext: AudioContext) =>
createEffectsMap([
{ name: 'reverb', node: createSimpleReverb(audioContext, 0.3, 1.5) },
{ name: 'lowpass', node: createLowPassFilter(audioContext, 8000) },
]),

warm: (audioContext: AudioContext) =>
createEffectsMap([
{ name: 'lowpass', node: createLowPassFilter(audioContext, 3000) },
{ name: 'gain', node: createGainEffect(audioContext, 1.2) },
]),

distorted: (audioContext: AudioContext) =>
createEffectsMap([
{ name: 'overdrive', node: createOverdrive(audioContext, 0.85) },
{ name: 'midpass', node: createBandPassFilter(audioContext, 800, 3000) },
{ name: 'lowpass', node: createLowPassFilter(audioContext, 6000) },
{ name: 'gain', node: createGainEffect(audioContext, 0.4) },
]),

test: (audioContext: AudioContext) =>
createEffectsMap([
{ name: 'gain', node: createGainEffect(audioContext, 0.3) },
]),

overdrive_only: (audioContext: AudioContext) =>
createEffectsMap([
{ name: 'overdrive', node: createOverdrive(audioContext, 0.9) },
]),
};
6 changes: 6 additions & 0 deletions packages/audiodocs/docs/core/base-audio-context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,12 @@ Creates [`StreamerNode`](/docs/sources/streamer-node).

#### Returns `StreamerNode`.

### `createWaveShaper`

Creates [`WaveShaperNode`](/docs/effects/wave-shaper-node).

#### Returns `WaveShaperNode`.

### `createWorkletNode` <MobileOnly />

Creates [`WorkletNode`](/docs/worklets/worklet-node).
Expand Down
57 changes: 57 additions & 0 deletions packages/audiodocs/docs/effects/wave-shaper-node.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
sidebar_position: 6
---

import AudioNodePropsTable from "@site/src/components/AudioNodePropsTable"
import { ReadOnly } from '@site/src/components/Badges';

# WaveShaperNode

The `WaveShaperNode` interface represents non-linear signal distortion effects.
Non-linear distortion is commonly used for both subtle non-linear warming, or more obvious distortion effects.

#### [`AudioNode`](/docs/core/audio-node#properties) properties

<AudioNodePropsTable numberOfInputs={1} numberOfOutputs={1} channelCount={2} channelCountMode={"max"} channelInterpretation={"speakers"} />

## Constructor

[`BaseAudioContext.createWaveShaper()`](/docs/core/base-audio-context#createwaveshaper)

## Properties

It inherits all properties from [`AudioNode`](/docs/core/audio-node#properties).

| Name | Type | Description |
| :--: | :--: | :---------- |
| `curve` | `Float32Array \| null` | The shaping curve used for waveshaping effect. |
| `oversample` | [`OverSampleType`](/docs/effects/wave-shaper-node#oversampletype) | Specifies what type of oversampling should be used when applying shaping curve. |

## Methods

`WaveShaperNode` does not define any additional methods.
It inherits all methods from [`AudioNode`](/docs/core/audio-node#methods).

## Remarks

#### `curve`
- Default value is null
- Contains at least two values.
- Subsequent modifications of curve have no effects. To change the curve, assign a new Float32Array object to this property.

#### `oversample`
- Default value `none`
- Value of `2x` or `4x` can increases quality of the effect, but in some cases it is better not to use oversampling for very accurate shaping curve.

### `OverSampleType`

<details>
<summary>Type definitions</summary>

```typescript
// Do not oversample | Oversample two times | Oversample four times
export type OverSampleType = 'none' | '2x' | '4x';
```

</details>

2 changes: 1 addition & 1 deletion packages/audiodocs/docs/other/web-audio-api-coverage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ sidebar_position: 2
| OscillatorNode | ✅ |
| PeriodicWave | ✅ |
| StereoPannerNode | ✅ |
| WaveShaperNode | ✅ |
| AudioContext | 🚧 | Available props and methods: `close`, `suspend`, `resume` |
| BaseAudioContext | 🚧 | Available props and methods: `currentTime`, `destination`, `sampleRate`, `state`, `decodeAudioData`, all create methods for available or partially implemented nodes |
| AudioListener | ❌ |
Expand All @@ -42,7 +43,6 @@ sidebar_position: 2
| MediaStreamAudioDestinationNode | ❌ |
| MediaStreamAudioSourceNode | ❌ |
| PannerNode | ❌ |
| WaveShaperNode | ❌ |

### Description

Expand Down
Binary file not shown.
Loading