Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
### High-performance audio engine for React Native based on web audio api specification

[![NPM latest](https://img.shields.io/npm/v/react-native-audio-api/latest)](https://www.npmjs.com/package/react-native-audio-api)
[![NPM next](https://img.shields.io/npm/v/react-native-audio-api/next)](https://www.npmjs.com/package/react-native-audio-api?activeTab=versions)
Comment thread
michalsek marked this conversation as resolved.
[![NPM nightly](https://img.shields.io/npm/v/react-native-audio-api/audio-api-nightly)](https://www.npmjs.com/package/react-native-audio-api?activeTab=versions)
[![github ci](https://github.com/software-mansion/react-native-audio-api/actions/workflows/ci.yml/badge.svg)](https://github.com/software-mansion/react-native-audio-api/actions/workflows/ci.yml)
[![NPM Audio Api publish nightly](https://github.com/software-mansion/react-native-audio-api/actions/workflows/npm-publish-nightly.yml/badge.svg)](https://github.com/software-mansion/react-native-audio-api/actions/workflows/npm-publish-nightly.yml)
Expand Down Expand Up @@ -40,12 +39,12 @@ check out the [Getting Started](https://docs.swmansion.com/react-native-audio-ap
- <sub>[![Released in 0.6.0](https://img.shields.io/badge/Released_in-0.6.0-green)](https://github.com/software-mansion/react-native-audio-api/releases/tag/0.6.0)</sub> **System configuration** 🛠️ <br />
Full control of system audio settings, remote controls, lock screen integration and most importantly configurable background modes <br />

- <sub>[![Released in 0.6.0](https://img.shields.io/badge/Released_in-0.6.0-green)](https://github.com/software-mansion/react-native-audio-api/releases/tag/0.6.0)</sub> **Connect audio param** 🤞 <br />
Ability to connect Audio nodes to audio params, which will allow for powerful and efficient modulation of audio parameters, creating effects like tremolo, vibrato or complex envelope followers. <br />

- **Microphone support** 🎙️ <br />
Grab audio data from device microphone or connected device, connect it to the audio graph or stream through the internet <br />

- **Connect audio param** 🤞 <br />
Ability to connect Audio nodes to audio params, which will allow for powerful and efficient modulation of audio parameters, creating effects like tremolo, vibrato or complex envelope followers. <br />

- **JS Audio Worklets** 🐎 <br />
Ability to run JS functions connected to the audio graph running on audio thread allowing for full customization of what happens to the audio signal.
<br />
Expand Down
243 changes: 243 additions & 0 deletions packages/audiodocs/docs/guides/create-your-own-effect.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
---
sidebar_position: 6
---

# Create your own effect

In this section, we will create our own [`pure C++ turbo-module`](https://reactnative.dev/docs/the-new-architecture/pure-cxx-modules) and use it to create custom processing node that can change sound whatever you want.

### Prerequisites

We highly encourage you to get familiar with [this guide](https://reactnative.dev/docs/the-new-architecture/pure-cxx-modules), since we will be using many similar concepts that are explained here.

## Generate files

We prepared a script that generates all of the boiler plate code for you.
Only parts that will be needed by you, are:
- customizing processor to your tasks
- configuring [`codegen`](https://reactnative.dev/docs/the-new-architecture/what-is-codegen) with your project
- writing native specific code to compile those files

```bash
npx rn-audioapi-custom-node-generator create -o # path where you want files to be generated, usually same level as android/ and ios/
```

## Analyzing generated files

You should see two directories:
- `shared/` - it contains c++ files (source code for custom effect and JSI layer - Host Objects, needed to communicate with JavaScript)
- `specs/` - defines typescript interface that will invoke c++ code in JavaScript

:::caution
Name of the file in `specs/` has to start with `Native` to be seen by codegen.
:::

The most important file is `MyProcessorNode.cpp`, it contains main processing part that directly manipulates raw data.

In this guide, we will edit files in order to achieve [`GainNode`](/effects/gain-node) functionality.
For the sake of a simplicity, we will use value as a raw `double` type, not wrapped in [`AudioParam`](/core/audio-param).

<details>
<summary>MyProcessorNode.h</summary>

```cpp
#pragma once
#include <audioapi/core/AudioNode.h>

namespace audioapi {
class AudioBus;

class MyProcessorNode : public AudioNode {
public:
explicit MyProcessorNode(BaseAudioContext *context);

protected:
void processNode(const std::shared_ptr<AudioBus> &bus,
int framesToProcess) override;

// highlight-start
private:
double gain; // value responsible for gain value
// highlight-end
};
} // namespace audioapi
```
</details>

<details>
<summary>MyProcessorNode.cpp</summary>

```cpp
#include "MyProcessorNode.h"
#include <audioapi/utils/AudioBus.h>
#include <audioapi/utils/AudioArray.h>

namespace audioapi {
MyProcessorNode::MyProcessorNode(BaseAudioContext *context)
//highlight-next-line
: AudioNode(context), gain(0.5) {
isInitialized_ = true;
}

void MyProcessorNode::processNode(const std::shared_ptr<AudioBus> &bus,
int framesToProcess) {
// highlight-start
for (int channel = 0; channel < bus->getNumberOfChannels(); ++channel) {
auto *audioArray = bus->getChannel(channel);
for (size_t i = 0; i < framesToProcess; ++i) {
// Apply gain to each sample in the audio array
(*audioArray)[i] *= gain;
}
}
// highlight-end
}
} // namespace audioapi
```
</details>

<details>
<summary>MyProcessorNodeHostObject.h</summary>

```cpp
#pragma once

#include "MyProcessorNode.h"
#include <audioapi/HostObjects/AudioNodeHostObject.h>

#include <memory>
#include <vector>

namespace audioapi {
using namespace facebook;

class MyProcessorNodeHostObject : public AudioNodeHostObject {
public:
explicit MyProcessorNodeHostObject(
const std::shared_ptr<MyProcessorNode> &node)
: AudioNodeHostObject(node) {
// highlight-start
addGetters(JSI_EXPORT_PROPERTY_GETTER(MyProcessorNodeHostObject, getter));
addSetters(JSI_EXPORT_PROPERTY_SETTER(MyProcessorNodeHostObject, setter));
// highlight-end
}

// highlight-start
JSI_PROPERTY_GETTER(getter) {
auto processorNode = std::static_pointer_cast<MyProcessorNode>(node_);
return {processorNode->someGetter()};
}
// highlight-end

// highlight-start
JSI_PROPERTY_SETTER(setter) {
auto processorNode = std::static_pointer_cast<MyProcessorNode>(node_);
processorNode->someSetter(value.getNumber());
}
// highlight-end
};
} // namespace audioapi
```
</details>

## Codegen

Onboarding codegen doesn't require anything special in regards to basic [react-native tutorial](https://reactnative.dev/docs/the-new-architecture/pure-cxx-modules#2-configure-codegen)

## Native files

### iOS

When it comes to iOS there is also nothing more than following [react-native tutorial](https://reactnative.dev/docs/the-new-architecture/pure-cxx-modules#ios)

### Android

Case with android is much different, because of the way android is compiled we need to compile our library with whole turbo-module.
Firstly, follow [the guide](https://reactnative.dev/docs/the-new-architecture/pure-cxx-modules#android), but replace `CmakeLists.txt` with this content:

```cmake
cmake_minimum_required(VERSION 3.13)

project(appmodules)

set(ROOT ${CMAKE_SOURCE_DIR}/../../../../..)
set(AUDIO_API_DIR ${ROOT}/node_modules/react-native-audio-api)

include(${REACT_ANDROID_DIR}/cmake-utils/ReactNative-application.cmake)

target_sources(${CMAKE_PROJECT_NAME} PRIVATE
${ROOT}/shared/NativeAudioProcessingModule.cpp
${ROOT}/shared/MyProcessorNode.cpp
${ROOT}/shared/MyProcessorNodeHostObject.cpp
)
Comment thread
michalsek marked this conversation as resolved.

target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC
${ROOT}/shared
${AUDIO_API_DIR}/common/cpp
)

add_library(react-native-audio-api SHARED IMPORTED)
string(TOLOWER ${CMAKE_BUILD_TYPE} BUILD_TYPE_LOWER)
# we need to find where original library was built
set_target_properties(react-native-audio-api PROPERTIES IMPORTED_LOCATION
${AUDIO_API_DIR}/android/build/intermediates/merged_native_libs/${BUILD_TYPE_LOWER}/merge${CMAKE_BUILD_TYPE}NativeLibs/out/lib/${CMAKE_ANDROID_ARCH_ABI}/libreact-native-audio-api.so
)
target_link_libraries(${CMAKE_PROJECT_NAME} react-native-audio-api android log)
```

## Final touches

Last part is to finally onboard your custom module to your app, by creating typescript interface that would map c++ layer.

```typescript
// types.ts
import { AudioNode, BaseAudioContext } from "react-native-audio-api";
import { IAudioNode, IBaseAudioContext } from "react-native-audio-api/lib/typescript/interfaces";

export interface IMyProcessorNode extends IAudioNode {
gain: number;
}

export class MyProcessorNode extends AudioNode {
constructor(context: BaseAudioContext, node: IMyProcessorNode) {
super(context, node);
}

public set gain(value: number) {
(this.node as IMyProcessorNode).gain = value;
}

public get gain(): number {
return (this.node as IMyProcessorNode).gain;
}
}

declare global {
var createCustomProcessorNode: (context: IBaseAudioContext) => IMyProcessorNode;
}
```

## Example

```tsx
import {
AudioContext,
OscillatorNode,
} from 'react-native-audio-api';
import { MyProcessorNode } from './types';

function App() {
const audioContext = new AudioContext();
const oscillator = audioContext.createOscillator();
// constructor is put in global scope
const processor = new MyProcessorNode(audioContext, global.createCustomProcessorNode(audioContext.context));
oscillator.connect(processor);
processor.connect(audioContext.destination);
oscillator.start(audioContext.currentTime);
}
```

**Check out fully working [demo app](https://github.com/software-mansion-labs/custom-processor-node-example)**

## What's next?

I’m not sure, but give yourself a pat on the back – you’ve earned it! More guides are on the way, so stay tuned! 🎼
2 changes: 1 addition & 1 deletion packages/audiodocs/docs/guides/see-your-sound.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -275,4 +275,4 @@ In this guide, we have learned how to extract audio data using [`AnalyserNode`](

## What's next?

I’m not sure, but give yourself a pat on the back – you’ve earned it! More guides are on the way, so stay tuned! 🎼
In [the next section](/guides/create-your-own-effect), we will learn how to create our own processing node, utilizing react native turbo-modules.
2 changes: 1 addition & 1 deletion packages/audiodocs/docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ const config = {
copyright: `All trademarks and copyrights belong to their respective owners.`,
},
prism: {
additionalLanguages: ['bash'],
additionalLanguages: ['bash', 'cmake'],
theme: lightCodeTheme,
darkTheme: darkCodeTheme,
},
Expand Down
19 changes: 19 additions & 0 deletions packages/custom-node-generator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Cli custom audio node generator

This library contains source code and templates needed to create a node for custom audio processing used by [react-native-audio-api](https://github.com/software-mansion/react-native-audio-api).

## Usage

```bash
npx rn-audioapi-custom-node-generator create -o # path
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
```

## Community Discord

[Join the Software Mansion Community Discord](https://discord.swmansion.com) to chat about React Native Audio Api or other Software Mansion libraries.

## Library created by Software Mansion

[![swm](https://logo.swmansion.com/logo?color=white&variant=desktop&width=150&tag=react-native-reanimated-github 'Software Mansion')](https://swmansion.com)

Since 2012 [Software Mansion](https://swmansion.com) is a software agency with experience in building web and mobile apps. We are Core React Native Contributors and experts in dealing with all kinds of React Native issues. We can help you build your next dream product – [Hire us](https://swmansion.com/contact/projects?utm_source=reanimated&utm_medium=readme).
30 changes: 30 additions & 0 deletions packages/custom-node-generator/bin/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env node

const { program } = require('commander');
const chalk = require('chalk');
const { generate } = require('../lib/generator');

program
.name('audio-api-node-generator')
.description('Generate template files for custom processor using react-native-audio-api.')
.version('0.0.2');


program
.command('create')
.option('-o, --output <path>', 'output directory', '.')
.action(async (options) => {
try {
const hasOutputFlag = process.argv.includes('-o') || process.argv.includes('--output');
if (!hasOutputFlag) {
console.log(chalk.yellow('Please specify an output directory with -o or --output:'));
return;
}
await generate(options.output, 'basic');
} catch (error) {
console.error(chalk.red('❌ Error:'), error.message);
process.exit(1);
}
});

program.parse();
55 changes: 55 additions & 0 deletions packages/custom-node-generator/lib/generator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const fs = require('fs-extra');
const path = require('path');
const chalk = require('chalk');

class FileGenerator {
constructor() {
this.templatesDir = path.join(__dirname, '../templates');
}

async generate(options) {
const { outputPath, template } = options;

const templatePath = path.join(this.templatesDir, template);

if (!await fs.pathExists(templatePath)) {
throw new Error(`Template not found`);
}

await fs.ensureDir(outputPath);

await this.copyTemplate(templatePath, outputPath);

console.log(chalk.green(`Generated files in: ${outputPath}`));
}

async copyTemplate(templatePath, targetPath) {
const files = await fs.readdir(templatePath);

for (const file of files) {
const srcPath = path.join(templatePath, file);
const destPath = path.join(targetPath, file);
const stat = await fs.stat(srcPath);

if (stat.isDirectory()) {
await fs.ensureDir(destPath);
await this.copyTemplate(srcPath, destPath);
} else {
await this.processFile(srcPath, destPath);
}
}
}

async processFile(srcPath, destPath) {
const content = await fs.readFile(srcPath, 'utf-8');

await fs.writeFile(destPath, content);
console.log(chalk.cyan(`Created: ${path.relative(process.cwd(), destPath)}`));
}
}

const generator = new FileGenerator();

module.exports = {
generate: (outputPath, template) => generator.generate({ outputPath, template }),
};
Loading
Loading