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
2 changes: 1 addition & 1 deletion packages/audiodocs/docs/effects/convolver-node.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Inherits all properties from [`AudioNode`](../core/audio-node.mdx#properties).

| Name | Type | Description |
| :----: | :----: | :-------- |
| `buffer` | [`AudioBuffer`](../sources/audio-buffer.mdx) | Associated AudioBuffer. |
| `buffer` | [`AudioBuffer`](../sources/audio-buffer.mdx) | Associated AudioBuffer. Setting it throws `NotSupportedError` unless the buffer has 1, 2 or 4 channels and the same sample rate as the context. |
| `normalize` | `boolean` | Whether the impulse response from the buffer will be scaled by an equal-power normalization when the buffer attribute is set. |

:::caution
Expand Down
2 changes: 1 addition & 1 deletion packages/react-native-audio-api/src/core/AudioBuffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export default class AudioBuffer implements AudioBufferLike {
options: AudioBufferOptions
): IAudioBuffer {
const { numberOfChannels = 1, length, sampleRate } = options;
if (numberOfChannels < 1 || numberOfChannels >= 32) {
if (numberOfChannels < 1 || numberOfChannels > 32) {
throw new NotSupportedError(
`The number of channels provided (${numberOfChannels}) is outside the range [1, 32]`
);
Expand Down
15 changes: 14 additions & 1 deletion packages/react-native-audio-api/src/core/ConvolverNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { ConvolverOptions } from '../types';
import type BaseAudioContext from './BaseAudioContext';
import AudioNode from './AudioNode';
import AudioBuffer from './AudioBuffer';
import { ConvolverOptionsValidator } from '../utils/validation';
import {
ConvolverOptionsValidator,
validateConvolverBufferChannelCount,
validateConvolverBufferSampleRate,
} from '../utils/validation';

export default class ConvolverNode extends AudioNode {
private _buffer: AudioBuffer | null = null;
Expand Down Expand Up @@ -31,6 +35,15 @@ export default class ConvolverNode extends AudioNode {
this._buffer = null;
return;
}

// Spec setter steps: the engine has no guard of its own, so an impulse
// response the convolution matrix is undefined for must be rejected here.
validateConvolverBufferChannelCount(buffer.numberOfChannels);
validateConvolverBufferSampleRate(
buffer.sampleRate,
this.context.sampleRate
);

(this.node as IConvolverNode).setBuffer(buffer.buffer);
this._buffer = buffer;
}
Expand Down
11 changes: 11 additions & 0 deletions packages/react-native-audio-api/src/utils/validation/convolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ export function validateConvolverBufferChannelCount(
}
}

export function validateConvolverBufferSampleRate(
bufferSampleRate: number,
contextSampleRate: number
): void {
if (bufferSampleRate !== contextSampleRate) {
throw new NotSupportedError(
`The sample rate of the impulse response for ConvolverNode buffer (${bufferSampleRate}) must match the sample rate of its context (${contextSampleRate}).`
);
}
}

export const ConvolverOptionsValidator: OptionsValidator<ConvolverOptions> = {
validate(options?: ConvolverOptions): void {
if (!options?.buffer) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export {
export {
ConvolverOptionsValidator,
validateConvolverBufferChannelCount,
validateConvolverBufferSampleRate,
} from './convolver';

export { OscillatorOptionsValidator } from './oscillator';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export default class AudioBuffer implements AudioBufferLike {
options: AudioBufferOptions
): globalThis.AudioBuffer {
const { numberOfChannels = 1, length, sampleRate } = options;
if (numberOfChannels < 1 || numberOfChannels >= 32) {
if (numberOfChannels < 1 || numberOfChannels > 32) {
throw new NotSupportedError(
`The number of channels provided (${numberOfChannels}) is outside the range [1, 32]`
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export default class AudioContext implements BaseAudioContext {
length: number,
sampleRate: number
): AudioBuffer {
if (numberOfChannels < 1 || numberOfChannels >= 32) {
if (numberOfChannels < 1 || numberOfChannels > 32) {
throw new NotSupportedError(
`The number of channels provided (${numberOfChannels}) is outside the range [1, 32]`
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export default class OfflineAudioContext implements BaseAudioContext {
length: number,
sampleRate: number
): AudioBuffer {
if (numberOfChannels < 1 || numberOfChannels >= 32) {
if (numberOfChannels < 1 || numberOfChannels > 32) {
throw new NotSupportedError(
`The number of channels provided (${numberOfChannels}) is outside the range [1, 32]`
);
Expand Down
92 changes: 92 additions & 0 deletions packages/react-native-audio-api/tests/convolver-buffer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import AudioBuffer from '../src/core/AudioBuffer';
import ConvolverNode from '../src/core/ConvolverNode';
import { NotSupportedError } from '../src/errors';
import type BaseAudioContext from '../src/core/BaseAudioContext';

const CONTEXT_SAMPLE_RATE = 48000;

function createContext(): BaseAudioContext {
return {
sampleRate: CONTEXT_SAMPLE_RATE,
context: {
createConvolver: () => ({
numberOfInputs: 1,
numberOfOutputs: 1,
normalize: true,
setBuffer: jest.fn(),
}),
},
} as unknown as BaseAudioContext;
}

function createBuffer(numberOfChannels: number, sampleRate: number) {
return new AudioBuffer({ numberOfChannels, length: 4, sampleRate });
}

beforeAll(() => {
globalThis.createAudioBuffer = jest.fn(
(numberOfChannels: number, length: number, sampleRate: number) => ({
length,
duration: length / sampleRate,
sampleRate,
numberOfChannels,
getChannelData: () => new Float32Array(length),
copyFromChannel: jest.fn(),
copyToChannel: jest.fn(),
})
) as unknown as typeof globalThis.createAudioBuffer;
});

describe('AudioBuffer channel count bounds', () => {
// The spec requires an implementation to support at least 32 channels.
it.each([1, 2, 32])('accepts %i channels', (numberOfChannels) => {
expect(
createBuffer(numberOfChannels, CONTEXT_SAMPLE_RATE).numberOfChannels
).toBe(numberOfChannels);
});

it.each([0, 33])('rejects %i channels', (numberOfChannels) => {
expect(() => createBuffer(numberOfChannels, CONTEXT_SAMPLE_RATE)).toThrow(
NotSupportedError
);
});
});

describe('ConvolverNode buffer setter', () => {
it.each([1, 2, 4])(
'accepts an impulse response with %i channels',
(channels) => {
const convolver = new ConvolverNode(createContext());
expect(() => {
convolver.buffer = createBuffer(channels, CONTEXT_SAMPLE_RATE);
}).not.toThrow();
expect(convolver.buffer?.numberOfChannels).toBe(channels);
}
);

it.each([3, 5, 6, 32])(
'rejects an impulse response with %i channels',
(channels) => {
const convolver = new ConvolverNode(createContext());
expect(() => {
convolver.buffer = createBuffer(channels, CONTEXT_SAMPLE_RATE);
}).toThrow(NotSupportedError);
expect(convolver.buffer).toBeNull();
}
);

it('rejects an impulse response whose sample rate differs from the context', () => {
const convolver = new ConvolverNode(createContext());
expect(() => {
convolver.buffer = createBuffer(1, CONTEXT_SAMPLE_RATE / 2);
}).toThrow(NotSupportedError);
expect(convolver.buffer).toBeNull();
});

it('accepts a null impulse response', () => {
const convolver = new ConvolverNode(createContext());
convolver.buffer = createBuffer(2, CONTEXT_SAMPLE_RATE);
convolver.buffer = null;
expect(convolver.buffer).toBeNull();
});
});