Skip to content
Draft
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
19 changes: 19 additions & 0 deletions .github/workflows/wpt-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: WPT Smoke

on:
pull_request:
workflow_dispatch:

jobs:
wpt-smoke:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup
uses: ./.github/actions/setup

- name: Build and run WPT smoke
working-directory: packages/react-native-audio-api
run: yarn wpt
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,12 @@ inline ConvolverOptions parseConvolverOptions(
}

if (optionsObject.hasProperty(runtime, "buffer")) {
auto bufferHostObject = optionsObject.getProperty(runtime, "buffer")
.getObject(runtime)
.asHostObject<AudioBufferHostObject>(runtime);
options.buffer = bufferHostObject->audioBuffer_;
auto bufferValue = optionsObject.getProperty(runtime, "buffer");
if (bufferValue.isObject() &&
bufferValue.getObject(runtime).isHostObject<AudioBufferHostObject>(runtime)) {
options.buffer =
bufferValue.getObject(runtime).asHostObject<AudioBufferHostObject>(runtime)->audioBuffer_;
}
}
return options;
}
Expand Down Expand Up @@ -285,10 +287,12 @@ inline AudioBufferSourceOptions parseAudioBufferSourceOptions(
AudioBufferSourceOptions options(parseBaseAudioBufferSourceOptions(runtime, optionsObject));

if (optionsObject.hasProperty(runtime, "buffer")) {
auto bufferHostObject = optionsObject.getProperty(runtime, "buffer")
.getObject(runtime)
.asHostObject<AudioBufferHostObject>(runtime);
options.buffer = bufferHostObject->audioBuffer_;
auto bufferValue = optionsObject.getProperty(runtime, "buffer");
if (bufferValue.isObject() &&
bufferValue.getObject(runtime).isHostObject<AudioBufferHostObject>(runtime)) {
options.buffer =
bufferValue.getObject(runtime).asHostObject<AudioBufferHostObject>(runtime)->audioBuffer_;
}
}

auto loopValue = optionsObject.getProperty(runtime, "loop");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#ifdef ANDROID
#ifdef RN_AUDIO_API_NODE
#include "NodeAudioPlayer.h"
#elif defined(ANDROID)
#include <audioapi/android/core/AudioPlayer.h>
#else
#include <audioapi/ios/core/IOSAudioPlayer.h>
Expand All @@ -25,7 +27,12 @@ AudioContext::~AudioContext() {

void AudioContext::initialize(const AudioDestinationNode *destination) {
BaseAudioContext::initialize(destination);
#ifdef ANDROID
#ifdef RN_AUDIO_API_NODE
audioPlayer_ = std::make_shared<NodeAudioPlayer>(
[this](DSPAudioBuffer *buf, int n) { processGraph(buf, n); },
getSampleRate(),
destination_->getChannelCount());
#elif defined(ANDROID)
audioPlayer_ = std::make_shared<AudioPlayer>(
[this](DSPAudioBuffer *buf, int n) { processGraph(buf, n); },
getSampleRate(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
#include <memory>

namespace audioapi {
#ifdef ANDROID
#ifdef RN_AUDIO_API_NODE
class NodeAudioPlayer;
#elif defined(ANDROID)
class AudioPlayer;
#else
class IOSAudioPlayer;
Expand All @@ -36,7 +38,9 @@ class AudioContext : public BaseAudioContext {
void initialize(const AudioDestinationNode *destination) final;

private:
#ifdef ANDROID
#ifdef RN_AUDIO_API_NODE
std::shared_ptr<NodeAudioPlayer> audioPlayer_;
#elif defined(ANDROID)
std::shared_ptr<AudioPlayer> audioPlayer_;
#else
std::shared_ptr<IOSAudioPlayer> audioPlayer_;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,19 +65,27 @@ void AudioBufferSourceNode::setBuffer(
context->getDisposer()->dispose(std::move(buffer_));
}

if (audioBuffer_ != nullptr) {
context->getDisposer()->dispose(std::move(audioBuffer_));
}

if (buffer == nullptr) {
loopEnd_ = 0;
channelCount_ = 1;

buffer_ = nullptr;
processor_->setBuffer(nullptr);
if (audioBuffer_ != nullptr) {
context->getDisposer()->dispose(std::move(audioBuffer_));
}
// Replace the (possibly shared with the previous AudioBuffer) output
// buffer with a fresh one so processNode() can output silence without
// touching the old buffer's data.
audioBuffer_ = std::make_shared<DSPAudioBuffer>(
RENDER_QUANTUM_SIZE, channelCount_, context->getSampleRate());
return;
}

if (audioBuffer_ != nullptr) {
context->getDisposer()->dispose(std::move(audioBuffer_));
}

buffer_ = buffer;
audioBuffer_ = audioBuffer;
channelCount_ = static_cast<int>(buffer_->getNumberOfChannels());
Expand Down
22 changes: 21 additions & 1 deletion packages/react-native-audio-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,17 @@
"module": "lib/module/index",
"react-native": "src/index",
"types": "lib/typescript/index.d.ts",
"exports": {
".": {
"types": "./lib/typescript/index.d.ts",
"react-native": "./src/index",
"node": "./wpt_tests/index.js",
"default": "./lib/module/index"
}
},
"files": [
"src/",
"wpt_tests/",
"development/react/",
"mock/",
"lib/",
Expand Down Expand Up @@ -70,6 +79,12 @@
"format:ios": "find ios/audioapi/ios -type f \\( -iname \"*.h\" -o -iname \"*.m\" -o -iname \"*.mm\" \\) | xargs clang-format -i",
"format:common": "find common/cpp/ \\( -path 'common/cpp/audioapi/libs' -prune -o -path 'common/cpp/audioapi/external' -prune -o -type f \\( -iname \"*.h\" -o -iname \"*.cpp\" -o -iname \"*.hpp\" \\) -print \\) | xargs clang-format -i",
"build": "bob build",
"node:build": "cmake-js compile -d wpt_tests",
"wpt:build": "yarn build && yarn node:build",
"wpt:only": "node ./wpt_tests/wpt/wpt-harness.mjs",
"wpt": "yarn wpt:build && yarn wpt:only",
"wpt:list": "node ./wpt_tests/wpt/wpt-harness.mjs --list",
"wpt:filter": "node ./wpt_tests/wpt/wpt-harness.mjs --filter",
"create:package": "./scripts/create-package.sh",
"prepack": "cp ../../README.md ./README.md",
"postpack": "rm ./README.md"
Expand Down Expand Up @@ -129,6 +144,9 @@
"@types/react-test-renderer": "^19.1.0",
"@types/semver": "7.7.1",
"babel-plugin-module-resolver": "^4.1.0",
"chalk": "^5.3.0",
"cmake-js": "^7.3.1",
"commander": "^14.0.0",
"commitlint": "17.0.2",
"del-cli": "^5.1.0",
"eslint": "^8.57.0",
Expand All @@ -145,13 +163,15 @@
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-standard": "^5.0.0",
"eslint-plugin-tsdoc": "^0.2.17",
"hermes-engine": "^0.11.0",
"jest": "^29.7.0",
"prettier": "^3.3.3",
"react": "19.2.3",
"react-native": "0.85.0",
"react-native-builder-bob": "0.33.1",
"turbo": "^1.10.7",
"typescript": "~6.0.3"
"typescript": "~6.0.3",
"wpt-runner": "^7.0.0"
},
"react-native-builder-bob": {
"source": "src",
Expand Down
37 changes: 35 additions & 2 deletions packages/react-native-audio-api/src/core/AudioBuffer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { AudioBufferLike, AudioBufferOptions } from '../types';
import { IAudioBuffer } from '../jsi-interfaces';
import { IndexSizeError, NotSupportedError } from '../errors';
import {
IndexSizeError,
NotSupportedError,
wrapFloat32ArrayView,
} from '../errors';

export default class AudioBuffer implements AudioBufferLike {
readonly length: number;
Expand Down Expand Up @@ -32,14 +36,17 @@ export default class AudioBuffer implements AudioBufferLike {
`The channel number provided (${channel}) is outside the range [0, ${this.numberOfChannels - 1}]`
);
}
return this.buffer.getChannelData(channel);
return wrapFloat32ArrayView(
this.buffer.getChannelData(channel)
) as Float32Array<ArrayBuffer>;
}

public copyFromChannel(
destination: Float32Array<ArrayBuffer>,
channelNumber: number,
startInChannel: number = 0
): void {
AudioBuffer.assertFloat32Array(destination, 'destination');
if (channelNumber < 0 || channelNumber >= this.numberOfChannels) {
throw new IndexSizeError(
`The channel number provided (${channelNumber}) is outside the range [0, ${this.numberOfChannels - 1}]`
Expand All @@ -60,6 +67,7 @@ export default class AudioBuffer implements AudioBufferLike {
channelNumber: number,
startInChannel: number = 0
): void {
AudioBuffer.assertFloat32Array(source, 'source');
if (channelNumber < 0 || channelNumber >= this.numberOfChannels) {
throw new IndexSizeError(
`The channel number provided (${channelNumber}) is outside the range [0, ${this.numberOfChannels - 1}]`
Expand All @@ -75,6 +83,31 @@ export default class AudioBuffer implements AudioBufferLike {
this.buffer.copyToChannel(source, channelNumber, startInChannel);
}

private static assertFloat32Array(value: unknown, name: string): void {
// Cross-realm Float32Arrays (e.g. from a jsdom window) fail instanceof,
// so also accept any object that looks like a Float32Array view.
const isFloat32View =
value instanceof Float32Array ||
(typeof value === 'object' &&
value !== null &&
ArrayBuffer.isView(value as ArrayBufferView) &&
(value as Float32Array).BYTES_PER_ELEMENT === 4 &&
(value.constructor as { name?: string })?.name === 'Float32Array');

if (!isFloat32View) {
throw new TypeError(`The provided ${name} is not a Float32Array`);
}

const backingBuffer = (value as Float32Array).buffer as {
constructor?: { name?: string };
};
if (backingBuffer?.constructor?.name === 'SharedArrayBuffer') {
throw new TypeError(
`The provided ${name} is backed by a SharedArrayBuffer, which is not allowed`
);
}
}

private static createBufferFromOptions(
options: AudioBufferOptions
): IAudioBuffer {
Expand Down
16 changes: 12 additions & 4 deletions packages/react-native-audio-api/src/core/AudioBufferSourceNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ export default class AudioBufferSourceNode extends AudioBufferBaseSourceNode {
private bufferHasBeenSet: boolean = false;

constructor(context: BaseAudioContext, options?: AudioBufferSourceOptions) {
const node = context.context.createBufferSource(options || {});
// The native layer expects the AudioBuffer HostObject, not the TS wrapper,
// so pass the buffer through the setter below instead of the options.
const { buffer, ...nativeOptions } = options ?? {};
const node = context.context.createBufferSource(nativeOptions);
super(context, node);

if (options?.buffer) {
this._buffer = options.buffer as AudioBuffer;
this.bufferHasBeenSet = true;
if (buffer != null) {
this.buffer = buffer as AudioBuffer;
}
}

Expand All @@ -36,6 +38,12 @@ export default class AudioBufferSourceNode extends AudioBufferBaseSourceNode {
return;
}

if (!(buffer instanceof AudioBuffer)) {
throw new TypeError(
'Failed to set buffer: the provided value is not of type AudioBuffer.'
);
}

if (this.bufferHasBeenSet) {
throw new InvalidStateError(
'The buffer can only be set once and cannot be changed afterwards.'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
type Float32ArrayViewFactory = (
buffer: ArrayBufferLike,
byteOffset: number,
length: number
) => Float32Array;

let float32ArrayViewFactory: Float32ArrayViewFactory | undefined;

export function setFloat32ArrayViewFactory(
factory: Float32ArrayViewFactory
): void {
float32ArrayViewFactory = factory;
}

export function wrapFloat32ArrayView(view: Float32Array): Float32Array {
if (float32ArrayViewFactory == null) {
return view;
}

return float32ArrayViewFactory(view.buffer, view.byteOffset, view.length);
}
4 changes: 4 additions & 0 deletions packages/react-native-audio-api/src/errors/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export {
setFloat32ArrayViewFactory,
wrapFloat32ArrayView,
} from './createFloat32ArrayView';
export { default as IndexSizeError } from './IndexSizeError';
export { default as InvalidAccessError } from './InvalidAccessError';
export { default as InvalidStateError } from './InvalidStateError';
Expand Down
2 changes: 2 additions & 0 deletions packages/react-native-audio-api/wpt_tests/.clangd
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
CompileFlags:
CompilationDatabase: build
Loading
Loading