# 开发音频插件 (/zh-CN/realtime-media/marketplace/build/create-extensions/audio-filter)

> For AI agents: see the complete documentation index at [llms.txt](/llms.txt).

<_PlatformTabsGroup>
<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="android" />
本文展示如何使用声网 SDK 提供的 API 开发一个音频插件。开发音频插件需要使用以下接口：

- `IAudioFilter`：实现音频数据的处理能力，包括接收、处理和返回处理完的音频帧。
- `IExtensionProvider`：将音频数据的处理能力封装为插件。

## 前提条件 [#前提条件]

开发前，请确保你的开发环境满足以下要求：

- Android Studio 3.0 或以上版本。
- Android API 级别 16 或以上。

## 准备开发环境 [#准备开发环境]

参考如下步骤将声网云市场的 API 添加到你的项目中：

1. 下载[视频 SDK v4.x](/zh-CN/realtime-media/rtc/reference/downloads)，然后解压下载的 SDK 包。
2. 将 SDK 包中的 `/rtc/sdk/low_level_api/include` 路径下的头文件拷贝至你的项目文件夹中。

## 开发音频插件 [#开发音频插件]

### 实现插件 [#实现插件]

音频插件通过 `IAudioFilter` 接口实现。该接口位于 `NGIAgoraMediaNode.h` 文件中。你需要实现 `IAudioFilter` 接口类的如下方法：

- [`adaptAudioFrame`](#adaptaudioframe)
- [`setEnabled`](#setenabled)
- [`isEnabled`](#isenabled)
- [`setProperty`](#setproperty)
- [`getProperty`](#getproperty)
- [`getName`](#getname)
- [`getPreferredSampleRate`](#getpreferredsamplerate)（可选）
- [`getPreferredChannelNumbers`](#getpreferredchannelnumbers)（可选）

#### adaptAudioFrame [#adaptaudioframe]

处理音频帧。该方法是 `IAudioFilter` 类的核心方法。调用该方法后，SDK 处理 `inAudioFrame` 中的音频数据，然后通过 `adaptedFrame` 返回处理后的数据。该方法目前仅支持输入输出 PCM 格式的音频数据。

```cpp
virtual bool adaptAudioFrame(const media::base::AudioPcmFrame& inAudioFrame,
                             media::base::AudioPcmFrame& adaptedFrame) = 0;
```

| 参数               | 描述                                                         |
| :----------------- | :----------------------------------------------------------- |
| `inAudioFrame`               | 输入参数。待处理的音频帧。 |
| `adaptedFrame` | 输出参数。处理后的音频帧。 |

#### setEnabled [#setenabled]

启用或关闭音频插件。

```cpp
virtual void setEnabled(bool enable) {}
```

| 参数 | 描述 |
| --- | --- |
| `enable` | [audio-filter-android-ios-macos-windows-html-0-1-1 details](#table-slot-audio-filter-android-ios-macos-windows-html-0-1-1) |

<a id="table-slot-audio-filter-android-ios-macos-windows-html-0-1-1"></a>

**audio-filter-android-ios-macos-windows-html-0-1-1 details**

是否启用音频插件：

- true: 启用音频插件。
- false: (默认) 关闭音频插件。

#### isEnabled [#isenabled]

检查音频插件是否已启用。

```cpp
virtual bool isEnabled() { return true; }
```

**返回值**

是否启用音频插件：

- true: 启用音频插件。
- false: (默认) 关闭音频插件。

#### setProperty [#setproperty]

设置音频插件属性。App 开发者调用 `setExtensionProperty` 时，SDK 会调用该方法。你需要返回音频插件的属性。

```cpp
size_t setProperty(const char* key, const void* buf, size_t buf_size)
```

| 参数     | 描述                                                         |
| :------- | :----------------------------------------------------------- |
| `key`      | 插件属性的 `key`。                                             |
| `buf`      | 插件属性 `key` 值对应的 buffer 地址，数据形式为 JSON 字符串。我们推荐使用第三方的 `nlohmann/json library` 开源库，帮助实现 C++ 的 struct 和 JSON 字符串之前的序列和反序列化。 |
| `buf_size` | 插件属性 buffer 的内存大小。                                 |

#### getProperty [#getproperty]

获取音频插件属性。App 开发者调用 `getProperty` 时，SDK 会调用该方法获取音频插件的属性。

```cpp
size_t getProperty(const char* key, char* property, size_t buf_size)
```

| 参数     | 描述                         |
| :------- | :--------------------------- |
| `key`      | 插件属性的 `key`。             |
| `property` | 插件属性指针。               |
| `buf_size` | 插件属性 buffer 的内存大小。 |

#### getName [#getname]

查询插件的名称。你需要在该方法中返回使用的音频插件的名称。

```cpp
virtual const char * getName() const = 0;
```

#### getPreferredSampleRate [#getpreferredsamplerate]

查询音频插件需要的音频采样率。

该方法为可选。如果你在该方法中返回一个指定的采样率，SDK 会将音频数据重采样成指定的采样率，然后传入音频插件。

```cpp
virtual int getPreferredSampleRate()  { return 0; };
```

#### getPreferredChannelNumbers [#getpreferredchannelnumbers]

查询音频插件需要的音频声道数。

该方法为可选。如果你在该方法中返回一个指定的声道数，SDK 会将音频数据重采样成指定的声道数，然后传入音频插件。

```cpp
virtual int getPreferredChannelNumbers()  { return 0; };
```

#### 示例代码 [#示例代码]

参考如下示例代码了解如何使用上述方法实现一个音频插件：

```cpp
// 在收到需要处理的音频数据后，调用 adaptAudioFrame 方法，对收到的音频数据进行处理。
bool ExtensionAudioFilter::adaptAudioFrame(const media::base::AudioPcmFrame& inAudioPcmFrame,
                                           media::base::AudioPcmFrame& adaptedPcmFrame) {
    return audioProcess_->processFrame(inAudioPcmFrame, adaptedPcmFrame) == 0;
}

// 调用 setProperty 设置音频插件参数。
int ExtensionAudioFilter::setProperty(const char* key, const void* buf, int buf_size) {
    std::string str_volume = "100";
    if (std::string(key) == "volume") {
        str_volume = std::string(static_cast<const char*>(buf), buf_size);
    }

    int int volume_ = atoi(str_volume.c_str());
    audioProcessor_->setVolume(int_volume_);
    return ERR_OK;
}

// 调用 getProperty 查询音频插件参数。
int ExtensionAudioFilter::getProperty(const char* key, void* buf, int buf_size) const override {return ERR_OK; }

// 调用 setEnabled 开启音频插件。
void ExtensionAudioFilter::setEnabled(bool enable) override { enabled_ = enable; }

// 调用 isEnabled 查询音频插件开启状态。
bool ExtensionAudioFilter::isEnabled() const override {return enabled_; }

// 在 getName 的返回值中传入插件名称。
const char* ExtensionAudioFilter::getName() const override { return "YourExtensionName"; }

// （可选）在 getPreferredSampleRate 的返回值中指定音频插件需要的音频采样率。
const char* ExtensionAudioFilter::getPreferredSampleRate() override { return 48000; }

// （可选）在 getPreferredChannelNumbers 的返回值中指定音频插件需要的音频声道数。
int ExtensionAudioFilter::getPreferredChannelNumbers() override { return 2; };
```

### 封装插件 [#封装插件]

封装插件通过 `IExtensionProvider` 接口类实现。该接口类位于 `NGIAgoraExtensionProvider.h` 文件中。你需要先实现这个接口类，并实现如下方法：

- [`setExtensionControl`](#setextensioncontrol)
- [`enumerateExtensions`](#enumerateextensions)
- [`createAudioFilter`](#createaudiofilter)

`IExtensionProvider` 是插件动态库的提供者，由全局唯一的名字标识。每一个插件动态库可以提供一个或多个有不同功能的插件，如音频前处理插件库可以提供不同功能的音频插件（Audio filter）。每个插件是一个具体的 Extension 对象。

#### setExtensionControl [#setextensioncontrol]

设置插件控制。

```cpp
virtual void setExtensionControl(IExtensionControl* control)
```

成功调用该方法后，你需要保存 SDK 在该方法中返回的 `IExtensionControl` 对象。该对象用于触发事件或日志，实现插件与 App 之间的交互。例如，如果你调用了 `IExtensionControl` 中的 `fireEvent` 方法：

```cpp
void ByteDanceProcessor::dataCallback(const char* data){
    if (control_ != nullptr) {
        control_->fireEvent(id_, "beauty", data);
    }
}
```

App 层在初始化 `RtcEngine` 时注册的 `IMediaExtensionObserver` 实例会收到该信息，并在 App 层触发如下回调：

```cpp
@Override
public void onEvent(String vendor, String key, String value) {
// 其中的 vendor 为注册插件时的 VENDOR_NAME，key/value 是插件消息的键值对
...
}
```

#### enumerateExtensions [#enumerateextensions]

提供所有支持封装的插件的信息。SDK 在加载插件时，会调用该方法向插件发送回调。收到该回调后，你需要通过返回值提供所有支持封装的插件的信息。

```cpp
virtual void enumerateExtensions(ExtensionMetaInfo* extension_list,
                                 int& extension_count) {
    (void) extension_list;
    extension_count = 0;
}
```

| 参数 | 描述 |
| --- | --- |
| `extension_list` | [audio-filter-android-ios-macos-windows-html-1-1-1 details](#table-slot-audio-filter-android-ios-macos-windows-html-1-1-1) |
| `extension_count` | 支持封装的插件的总数量。 |

<a id="table-slot-audio-filter-android-ios-macos-windows-html-1-1-1"></a>

**audio-filter-android-ios-macos-windows-html-1-1-1 details**

插件的信息，包括插件类型和插件名称。

支持的音频插件类型有：

- `AUDIO_RECORDING_LOCAL_PLAYBACK_FILTER`（上行）
- `AUDIO_POST_PROCESSING_FILTER`（上行）
- `AUDIO_REMOTE_USER_PLAYBACK_FILTER`（上行）

插件名由插件服务商指定，需要在 `IExtensionProvider` 对象内唯一。插件名为 String 型，支持如下字符：

- 26 个小写字母 `a`~`z`
- 数字和下划线（不能是第一个或最后一个）
如果插件名不符合规则，会无法通过编译。

其中，插件的信息定义如下：

```cpp
// 插件类型指插件在音视频传输通道中的位置，分类如下：
enum EXTENSION_TYPE {
    // 已废弃，请改用 AUDIO_RECORDING_LOCAL_PLAYBACK_FILTER 或 AUDIO_POST_PROCESSING_FILTER
    AUDIO_FILTER,
    // 视频前处理插件
    VIDEO_PRE_PROCESSING_FILTER,
    // 视频后处理插件
    VIDEO_POST_PROCESSING_FILTER,
    // 预留参数
    AUDIO_SINK,
    // 预留参数
    VIDEO_SINK,
    // 用于处理本地采集（如耳返采集）的，用于本地播放的音频数据
    AUDIO_RECORDING_LOCAL_PLAYBACK_FILTER = 10000,
    // 经过 3A 算法后的本地音频数据后处理
    AUDIO_POST_PROCESSING_FILTER = 10001,
    // 用于对远端用户的音频数据进行处理
    AUDIO_REMOTE_USER_PLAYBACK_FILTER = 10002,
};

// 插件的信息，包括插件类型和插件名称
struct ExtensionMetaInfo {
    EXTENSION_TYPE type;
    const char* extension_name;
};
```

如果你在 `enumerateExtensions` 方法中返回的插件类型为 `AUDIO_FILTER`，则在 App 开发者初始化 `RtcEngine` 并创建 `IExtensionProvider` 对象后，SDK 会调用 `createAudioFilter` 方法。

#### createAudioFilter [#createaudiofilter]

创建音频插件。SDK 调用该方法后，你需要返回 `IAudioFilter` 实例。

```cpp
virtual agora_refptr<IAudioFilter> createAudioFilter()
```

成功创建 `IAudioFilter` 实例后，音频插件会在合适的时机通过 `IAudioFilter` 类对输入的音频数据进行处理。

#### 示例代码 [#示例代码-1]

参考如下示例代码了解如何使用上述方法封装音频插件：

```cpp
void ExtensionProvider::enumerateExtensions(ExtensionMetaInfo* extension_list,
                                            int& extension_count) {
    extension_count = 1;
    ExtensionMetaInfo i;
    i.type = EXTENSION_TYPE::AUDIO_RECORDING_LOCAL_PLAYBACK_FILTER;
    i.extension_name = "YourExtensionName";
    extension_list[0] = i;
}

agora_refptr<agora::rtc::IAudioFilter> ExtensionAudioProvider::createAudioFilter() {
    PRINTF_ERROR("ExtensionAudioProvider::createAudioFilter");
    auto audioFilter = new agora::RefCountedObject<agora::extension::ExtensionAudioFilter>(audioProcessor_);
    return audioFilter;
}

void ExtensionAudioProvider::setExtensionControl(rtc::IExtensionControl* control){
    audioProcessor_->setExtensionControl(control);
}
```

## 打包音频插件 [#打包音频插件]

完成插件开发后，你需要对其进行注册、打包，并将最终的 `.aar` 或 `.so` 文件，连同一个包含了插件名称、服务商名称和 Filter 名称的文件提交给声网进行验证。

### 注册插件 [#注册插件]

插件通过宏 `REGISTER_AGORA_EXTENSION_PROVIDER` 进行注册，该宏位于 `AgoraExtensionProviderEntry.h` 文件中。

你需要在插件的入口使用这个宏。SDK 在加载插件时，该宏会自动向 SDK 注册你的插件。注意填入 `PROVIDER_NAME` 时不要填写标点符号。示例：

```cpp
REGISTER_AGORA_EXTENSION_PROVIDER(ByteDance, agora::extension::ExtensionProvider);
```

在 `CMakeLists.txt` 文件中，按照下表指定 SDK 包中 `libagora-rtc-sdk-jni.so` 文件的存储路径：

| 依赖文件 | 存储路径 |
| ---------------- | ---------------- |
| 64 位的 `libagora-rtc-sdk-jni.so`      | `AgoraWithByteDanceAndroid/agora-bytedance/src/main/agoraLibs/arm64-v8a`     |
| 32 位的 `libagora-rtc-sdk-jni.so` | `AgoraWithByteDanceAndroid/agora-bytedance/src/main/agoraLibs/arm64-v7a` |

### 提供插件信息 [#提供插件信息]

新建一个 `.java` 或 `.md` 文件，并在其中填入如下信息：

- `EXTENSION_NAME`：插件包的名称，如 `libagora-bytedance.so` 文件的 `EXTENSION_NAME` 就是 `agora-bytedance`。
- `EXTENSION_VENDOR_NAME`：服务商名称，即插件的 `Provider` 对象名称，用于在 `.cpp` 文件中进行注册。
- `EXTENSION_FILTER_NAME`：`ExtensionProvider.h` 文件中定义的 Filter 名称。

## 示例项目 [#示例项目]

声网提供一个 Android 插件开发的示例项目 [agora-simple-filter](https://github.com/AgoraIO/API-Examples/tree/main/Android/APIExample/agora-simple-filter) 供你参考。
<_PlatformProcessedMarker close="true" />

<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="ios" />
本文展示如何使用声网 SDK 提供的 API 开发一个音频插件。

开发音频插件需要使用以下接口：

- `IAudioFilter`：实现音频数据的处理能力，包括接收、处理和返回处理完的音频帧。
- `IExtensionProvider`：将音频数据的处理能力封装为插件。

## 前提条件 [#前提条件-1]

开发前，请确保你的开发环境满足以下要求：

- Xcode 9.0 或以上版本。
- iOS 9.0 或以上版本的设备。

## 准备开发环境 [#准备开发环境-1]

参考如下步骤将声网云市场的 API 添加到你的项目中：

1. 下载[视频 SDK v4.x](/zh-CN/realtime-media/rtc/reference/downloads)，然后解压下载的 SDK 包。
2. 打开 **Xcode**，进入 **TARGETS** > **Project Name** > **General** > **Frameworks, Libraries, and Embedded Content** 菜单，点击 **+** > **Add Other...** > **Add Files**，将安装包中的 `/libs/AgoraRtcKit.framework` 文件添加至你的项目中。确保 **Embed** 属性设置为 **Do Not Embed**。

## 开发音频插件 [#开发音频插件-1]

### 实现插件 [#实现插件-1]

音频插件通过 `IAudioFilter` 接口实现。该接口位于 `NGIAgoraMediaNode.h` 文件中。你需要实现 `IAudioFilter` 接口类的如下方法：

- [`adaptAudioFrame`](#adaptaudioframe-1)
- [`setEnabled`](#setenabled-1)
- [`isEnabled`](#isenabled-1)
- [`setProperty`](#setproperty-1)
- [`getProperty`](#getproperty-1)
- [`getName`](#getname-1)
- [`getPreferredSampleRate`](#getpreferredsamplerate-1)（可选）
- [`getPreferredChannelNumbers`](#getpreferredchannelnumbers-1)（可选）

#### adaptAudioFrame [#adaptaudioframe-1]

处理音频帧。该方法是 `IAudioFilter` 类的核心方法。调用该方法后，SDK 处理 `inAudioFrame` 中的音频数据，然后通过 `adaptedFrame` 返回处理后的数据。该方法目前仅支持输入输出 PCM 格式的音频数据。

```cpp
virtual bool adaptAudioFrame(const media::base::AudioPcmFrame& inAudioFrame,
                             media::base::AudioPcmFrame& adaptedFrame) = 0;
```

| 参数               | 描述                                                         |
| :----------------- | :----------------------------------------------------------- |
| `inAudioFrame`               | 输入参数。待处理的音频帧。 |
| `adaptedFrame` | 输出参数。处理后的音频帧。 |

#### setEnabled [#setenabled-1]

启用或关闭音频插件。

```cpp
virtual void setEnabled(bool enable) {}
```

| 参数 | 描述 |
| --- | --- |
| `enable` | [audio-filter-android-ios-macos-windows-html-0-1-1 details](#table-slot-audio-filter-android-ios-macos-windows-html-0-1-1-1) |

<a id="table-slot-audio-filter-android-ios-macos-windows-html-0-1-1-1"></a>

**audio-filter-android-ios-macos-windows-html-0-1-1 details**

是否启用音频插件：

- true: 启用音频插件。
- false: (默认) 关闭音频插件。

#### isEnabled [#isenabled-1]

检查音频插件是否已启用。

```cpp
virtual bool isEnabled() { return true; }
```

**返回值**

是否启用音频插件：

- true: 启用音频插件。
- false: (默认) 关闭音频插件。

#### setProperty [#setproperty-1]

设置音频插件属性。App 开发者调用 `setExtensionPropertyWithVendor` 时，SDK 会调用该方法。你需要返回音频插件的属性。

```cpp
size_t setProperty(const char* key, const void* buf, size_t buf_size)
```

| 参数     | 描述                                                         |
| :------- | :----------------------------------------------------------- |
| `key`      | 插件属性的 `key`。                                             |
| `buf`      | 插件属性 `key` 值对应的 buffer 地址，数据形式为 JSON 字符串。我们推荐使用第三方的 `nlohmann/json library` 开源库，帮助实现 C++ 的 struct 和 JSON 字符串之前的序列和反序列化。 |
| `buf_size` | 插件属性 buffer 的内存大小。                                 |

#### getProperty [#getproperty-1]

获取音频插件属性。App 开发者调用 `getProperty` 时，SDK 会调用该方法获取音频插件的属性。

```cpp
size_t getProperty(const char* key, char* property, size_t buf_size)
```

| 参数     | 描述                         |
| :------- | :--------------------------- |
| `key`      | 插件属性的 `key`。             |
| `property` | 插件属性指针。               |
| `buf_size` | 插件属性 buffer 的内存大小。 |

#### getName [#getname-1]

查询插件的名称。你需要在该方法中返回使用的音频插件的名称。

```cpp
virtual const char * getName() const = 0;
```

#### getPreferredSampleRate [#getpreferredsamplerate-1]

查询音频插件需要的音频采样率。

该方法为可选。如果你在该方法中返回一个指定的采样率，SDK 会将音频数据重采样成指定的采样率，然后传入音频插件。

```cpp
virtual int getPreferredSampleRate()  { return 0; };
```

#### getPreferredChannelNumbers [#getpreferredchannelnumbers-1]

查询音频插件需要的音频声道数。

该方法为可选。如果你在该方法中返回一个指定的声道数，SDK 会将音频数据重采样成指定的声道数，然后传入音频插件。

```cpp
virtual int getPreferredChannelNumbers()  { return 0; };
```

#### 示例代码 [#示例代码-2]

参考如下示例代码了解如何使用上述方法实现一个音频插件：

```cpp
// 在收到需要处理的音频数据后，调用 adaptAudioFrame 方法，对收到的音频数据进行处理。
bool ExtensionAudioFilter::adaptAudioFrame(const media::base::AudioPcmFrame& inAudioPcmFrame,
                                           media::base::AudioPcmFrame& adaptedPcmFrame) {
    return audioProcess_->processFrame(inAudioPcmFrame, adaptedPcmFrame) == 0;
}

// 调用 setProperty 设置音频插件参数。
int ExtensionAudioFilter::setProperty(const char* key, const void* buf, int buf_size) {
    std::string str_volume = "100";
    if (std::string(key) == "volume") {
        str_volume = std::string(static_cast<const char*>(buf), buf_size);
    }

    int int volume_ = atoi(str_volume.c_str());
    audioProcessor_->setVolume(int_volume_);
    return ERR_OK;
}

// 调用 getProperty 查询音频插件参数。
int ExtensionAudioFilter::getProperty(const char* key, void* buf, int buf_size) const override {return ERR_OK; }

// 调用 setEnabled 开启音频插件。
void ExtensionAudioFilter::setEnabled(bool enable) override { enabled_ = enable; }

// 调用 isEnabled 查询音频插件开启状态。
bool ExtensionAudioFilter::isEnabled() const override {return enabled_; }

// 在 getName 的返回值中传入插件名称。
const char* ExtensionAudioFilter::getName() const override { return "YourExtensionName"; }

// （可选）在 getPreferredSampleRate 的返回值中指定音频插件需要的音频采样率。
const char* ExtensionAudioFilter::getPreferredSampleRate() override { return 48000; }

// （可选）在 getPreferredChannelNumbers 的返回值中指定音频插件需要的音频声道数。
int ExtensionAudioFilter::getPreferredChannelNumbers() override { return 2; };
```

### 封装插件 [#封装插件-1]

封装插件通过 `IExtensionProvider` 接口类实现。该接口类位于 `NGIAgoraExtensionProvider.h` 文件中。你需要先实现这个接口类，并实现如下方法：

- [`setExtensionControl`](#setextensioncontrol-1)
- [`enumerateExtensions`](#enumerateextensions-1)
- [`createAudioFilter`](#createaudiofilter-1)

`IExtensionProvider` 是插件动态库的提供者，由全局唯一的名字标识。每一个插件动态库可以提供一个或多个有不同功能的插件，如音频前处理插件库可以提供不同功能的音频插件（Audio filter）。每个插件是一个具体的 Extension 对象。

#### setExtensionControl [#setextensioncontrol-1]

设置插件控制。

```cpp
virtual void setExtensionControl(IExtensionControl* control)
```

成功调用该方法后，你需要保存 SDK 在该方法中返回的 `IExtensionControl` 对象。该对象用于触发事件或日志，实现插件与 App 之间的交互。例如，如果你调用了 `IExtensionControl` 中的 `fireEvent` 方法：

```cpp
void ByteDanceProcessor::dataCallback(const char* data){
    if (control_ != nullptr) {
        control_->fireEvent(id_, "beauty", data);
    }
}
```

App 层在初始化 `AgoraRtcEngineKit` 时注册的 `AgoraMediaFilterEventDelegate` 实例会收到该信息，并在 App 层触发如下回调：

```cpp
- (void)onEvent:(NSString *)provider extension:(NSString *)extension key:(NSString *)key value:(NSString *)value {
// 其中的 vendor 为注册插件时的 VENDOR_NAME，key/value 是插件消息的键值对
...
}
```

#### enumerateExtensions [#enumerateextensions-1]

提供所有支持封装的插件的信息。SDK 在加载插件时，会调用该方法向插件发送回调。收到该回调后，你需要通过返回值提供所有支持封装的插件的信息。

```cpp
virtual void enumerateExtensions(ExtensionMetaInfo* extension_list,
                                 int& extension_count) {
    (void) extension_list;
    extension_count = 0;
}
```

| 参数 | 描述 |
| --- | --- |
| `extension_list` | [audio-filter-android-ios-macos-windows-html-1-1-1 details](#table-slot-audio-filter-android-ios-macos-windows-html-1-1-1-1) |
| `extension_count` | 支持封装的插件的总数量。 |

<a id="table-slot-audio-filter-android-ios-macos-windows-html-1-1-1-1"></a>

**audio-filter-android-ios-macos-windows-html-1-1-1 details**

插件的信息，包括插件类型和插件名称。

支持的音频插件类型有：

- `AUDIO_RECORDING_LOCAL_PLAYBACK_FILTER`（上行）
- `AUDIO_POST_PROCESSING_FILTER`（上行）
- `AUDIO_REMOTE_USER_PLAYBACK_FILTER`（上行）

插件名由插件服务商指定，需要在 `IExtensionProvider` 对象内唯一。插件名为 String 型，支持如下字符：

- 26 个小写字母 `a`~`z`
- 数字和下划线（不能是第一个或最后一个）
如果插件名不符合规则，会无法通过编译。

其中，插件的信息定义如下：

```cpp
// 插件类型指插件在音视频传输通道中的位置，分类如下：
enum EXTENSION_TYPE {
    // 已废弃，请改用 AUDIO_RECORDING_LOCAL_PLAYBACK_FILTER 或 AUDIO_POST_PROCESSING_FILTER
    AUDIO_FILTER,
    // 视频前处理插件
    VIDEO_PRE_PROCESSING_FILTER,
    // 视频后处理插件
    VIDEO_POST_PROCESSING_FILTER,
    // 预留参数
    AUDIO_SINK,
    // 预留参数
    VIDEO_SINK,
    // 用于处理本地采集（如耳返采集）的，用于本地播放的音频数据
    AUDIO_RECORDING_LOCAL_PLAYBACK_FILTER = 10000,
    // 经过 3A 算法后的本地音频数据后处理
    AUDIO_POST_PROCESSING_FILTER = 10001,
    // 用于对远端用户的音频数据进行处理
    AUDIO_REMOTE_USER_PLAYBACK_FILTER = 10002,
};

// 插件的信息，包括插件类型和插件名称
struct ExtensionMetaInfo {
    EXTENSION_TYPE type;
    const char* extension_name;
};
```

如果你在 `enumerateExtensions` 方法中返回的插件类型为 `AUDIO_FILTER`，则在 App 开发者初始化 `RtcEngine` 并创建 `IExtensionProvider` 对象后，SDK 会调用 `createAudioFilter` 方法。

#### createAudioFilter [#createaudiofilter-1]

创建音频插件。SDK 调用该方法后，你需要返回 `IAudioFilter` 实例。

```cpp
virtual agora_refptr<IAudioFilter> createAudioFilter()
```

成功创建 `IAudioFilter` 实例后，音频插件会在合适的时机通过 `IAudioFilter` 类对输入的音频数据进行处理。

#### 示例代码 [#示例代码-3]

参考如下示例代码了解如何使用上述方法封装音频插件：

```cpp
void ExtensionProvider::enumerateExtensions(ExtensionMetaInfo* extension_list,
                                            int& extension_count) {
    extension_count = 1;
    ExtensionMetaInfo i;
    i.type = EXTENSION_TYPE::AUDIO_RECORDING_LOCAL_PLAYBACK_FILTER;
    i.extension_name = "YourExtensionName";
    extension_list[0] = i;
}

agora_refptr<agora::rtc::IAudioFilter> ExtensionAudioProvider::createAudioFilter() {
    PRINTF_ERROR("ExtensionAudioProvider::createAudioFilter");
    auto audioFilter = new agora::RefCountedObject<agora::extension::ExtensionAudioFilter>(audioProcessor_);
    return audioFilter;
}

void ExtensionAudioProvider::setExtensionControl(rtc::IExtensionControl* control){
    audioProcessor_->setExtensionControl(control);
}
```

## 打包音频插件 [#打包音频插件-1]

完成插件开发后，你需要对其进行注册、打包，并将最终的 `.framework` 或 `.xcframework` 文件，连同一个包含了插件名称、服务商名称和 Filter 名称的文件提交给声网进行验证。

插件通过宏 `REGISTER_AGORA_EXTENSION_PROVIDER` 进行注册，该宏位于 `AgoraExtensionProviderEntry.h` 文件中。

你需要在插件的入口使用这个宏。SDK 在加载插件时，该宏会自动向 SDK 注册你的插件。注意填入 `PROVIDER_NAME` 时不要填写标点符号。示例：

```cpp
REGISTER_AGORA_EXTENSION_PROVIDER(ByteDance, agora::extension::ExtensionProvider);
```

## 示例项目 [#示例项目-1]

声网提供一个 iOS 插件开发的示例项目 [SimpleFilter](https://github.com/AgoraIO/API-Examples/tree/4.0.0-GA/iOS/SimpleFilter) 供你参考。
<_PlatformProcessedMarker close="true" />

<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="macos" />
本文展示如何使用声网 SDK 提供的 API 开发一个音频插件。

开发音频插件需要使用以下接口：

- `IAudioFilter`：实现音频数据的处理能力，包括接收、处理和返回处理完的音频帧。
- `IExtensionProvider`：将音频数据的处理能力封装为插件。

## 前提条件 [#前提条件-2]

开发前，请确保你的开发环境满足以下要求：

- Xcode 9.0 或以上版本。
- macOS 10.10 或以上版本的设备。

## 准备开发环境 [#准备开发环境-2]

参考如下步骤将声网云市场的 API 添加到你的项目中：

1. 下载[视频 SDK v4.x](/zh-CN/realtime-media/rtc/reference/downloads)，然后解压下载的 SDK 包。
2. 打开 **Xcode**，进入 **TARGETS** > **Project Name** > **General** > **Frameworks, Libraries, and Embedded Content** 菜单，点击 **+** > **Add Other...** > **Add Files**，将安装包中的 `/libs/AgoraRtcKit.framework` 文件添加至你的项目中。确保 **Embed** 属性设置为 **Do Not Embed**。

## 开发音频插件 [#开发音频插件-2]

### 实现插件 [#实现插件-2]

音频插件通过 `IAudioFilter` 接口实现。该接口位于 `NGIAgoraMediaNode.h` 文件中。你需要实现 `IAudioFilter` 接口类的如下方法：

- [`adaptAudioFrame`](#adaptaudioframe-2)
- [`setEnabled`](#setenabled-2)
- [`isEnabled`](#isenabled-2)
- [`setProperty`](#setproperty-2)
- [`getProperty`](#getproperty-2)
- [`getName`](#getname-2)
- [`getPreferredSampleRate`](#getpreferredsamplerate-2)（可选）
- [`getPreferredChannelNumbers`](#getpreferredchannelnumbers-2)（可选）

#### adaptAudioFrame [#adaptaudioframe-2]

处理音频帧。该方法是 `IAudioFilter` 类的核心方法。调用该方法后，SDK 处理 `inAudioFrame` 中的音频数据，然后通过 `adaptedFrame` 返回处理后的数据。该方法目前仅支持输入输出 PCM 格式的音频数据。

```cpp
virtual bool adaptAudioFrame(const media::base::AudioPcmFrame& inAudioFrame,
                             media::base::AudioPcmFrame& adaptedFrame) = 0;
```

| 参数               | 描述                                                         |
| :----------------- | :----------------------------------------------------------- |
| `inAudioFrame`               | 输入参数。待处理的音频帧。 |
| `adaptedFrame` | 输出参数。处理后的音频帧。 |

#### setEnabled [#setenabled-2]

启用或关闭音频插件。

```cpp
virtual void setEnabled(bool enable) {}
```

| 参数 | 描述 |
| --- | --- |
| `enable` | [audio-filter-android-ios-macos-windows-html-0-1-1 details](#table-slot-audio-filter-android-ios-macos-windows-html-0-1-1-2) |

<a id="table-slot-audio-filter-android-ios-macos-windows-html-0-1-1-2"></a>

**audio-filter-android-ios-macos-windows-html-0-1-1 details**

是否启用音频插件：

- true: 启用音频插件。
- false: (默认) 关闭音频插件。

#### isEnabled [#isenabled-2]

检查音频插件是否已启用。

```cpp
virtual bool isEnabled() { return true; }
```

**返回值**

是否启用音频插件：

- true: 启用音频插件。
- false: (默认) 关闭音频插件。

#### setProperty [#setproperty-2]

设置音频插件属性。App 开发者调用 `setExtensionPropertyWithVendor` 时，SDK 会调用该方法。你需要返回音频插件的属性。

```cpp
size_t setProperty(const char* key, const void* buf, size_t buf_size)
```

| 参数     | 描述                                                         |
| :------- | :----------------------------------------------------------- |
| `key`      | 插件属性的 `key`。                                             |
| `buf`      | 插件属性 `key` 值对应的 buffer 地址，数据形式为 JSON 字符串。我们推荐使用第三方的 `nlohmann/json library` 开源库，帮助实现 C++ 的 struct 和 JSON 字符串之前的序列和反序列化。 |
| `buf_size` | 插件属性 buffer 的内存大小。                                 |

#### getProperty [#getproperty-2]

获取音频插件属性。App 开发者调用 `getProperty` 时，SDK 会调用该方法获取音频插件的属性。

```cpp
size_t getProperty(const char* key, char* property, size_t buf_size)
```

| 参数     | 描述                         |
| :------- | :--------------------------- |
| `key`      | 插件属性的 `key`。             |
| `property` | 插件属性指针。               |
| `buf_size` | 插件属性 buffer 的内存大小。 |

#### getName [#getname-2]

查询插件的名称。你需要在该方法中返回使用的音频插件的名称。

```cpp
virtual const char * getName() const = 0;
```

#### getPreferredSampleRate [#getpreferredsamplerate-2]

查询音频插件需要的音频采样率。

该方法为可选。如果你在该方法中返回一个指定的采样率，SDK 会将音频数据重采样成指定的采样率，然后传入音频插件。

```cpp
virtual int getPreferredSampleRate()  { return 0; };
```

#### getPreferredChannelNumbers [#getpreferredchannelnumbers-2]

查询音频插件需要的音频声道数。

该方法为可选。如果你在该方法中返回一个指定的声道数，SDK 会将音频数据重采样成指定的声道数，然后传入音频插件。

```cpp
virtual int getPreferredChannelNumbers()  { return 0; };
```

#### 示例代码 [#示例代码-4]

参考如下示例代码了解如何使用上述方法实现一个音频插件：

```cpp
// 在收到需要处理的音频数据后，调用 adaptAudioFrame 方法，对收到的音频数据进行处理。
bool ExtensionAudioFilter::adaptAudioFrame(const media::base::AudioPcmFrame& inAudioPcmFrame,
                                           media::base::AudioPcmFrame& adaptedPcmFrame) {
    return audioProcess_->processFrame(inAudioPcmFrame, adaptedPcmFrame) == 0;
}

// 调用 setProperty 设置音频插件参数。
int ExtensionAudioFilter::setProperty(const char* key, const void* buf, int buf_size) {
    std::string str_volume = "100";
    if (std::string(key) == "volume") {
        str_volume = std::string(static_cast<const char*>(buf), buf_size);
    }

    int int volume_ = atoi(str_volume.c_str());
    audioProcessor_->setVolume(int_volume_);
    return ERR_OK;
}

// 调用 getProperty 查询音频插件参数。
int ExtensionAudioFilter::getProperty(const char* key, void* buf, int buf_size) const override {return ERR_OK; }

// 调用 setEnabled 开启音频插件。
void ExtensionAudioFilter::setEnabled(bool enable) override { enabled_ = enable; }

// 调用 isEnabled 查询音频插件开启状态。
bool ExtensionAudioFilter::isEnabled() const override {return enabled_; }

// 在 getName 的返回值中传入插件名称。
const char* ExtensionAudioFilter::getName() const override { return "YourExtensionName"; }

// （可选）在 getPreferredSampleRate 的返回值中指定音频插件需要的音频采样率。
const char* ExtensionAudioFilter::getPreferredSampleRate() override { return 48000; }

// （可选）在 getPreferredChannelNumbers 的返回值中指定音频插件需要的音频声道数。
int ExtensionAudioFilter::getPreferredChannelNumbers() override { return 2; };
```

### 封装插件 [#封装插件-2]

封装插件通过 `IExtensionProvider` 接口类实现。该接口类位于 `NGIAgoraExtensionProvider.h` 文件中。你需要先实现这个接口类，并实现如下方法：

- [`setExtensionControl`](#setextensioncontrol-2)
- [`enumerateExtensions`](#enumerateextensions-2)
- [`createAudioFilter`](#createaudiofilter-2)

`IExtensionProvider` 是插件动态库的提供者，由全局唯一的名字标识。每一个插件动态库可以提供一个或多个有不同功能的插件，如音频前处理插件库可以提供不同功能的音频插件（Audio filter）。每个插件是一个具体的 Extension 对象。

#### setExtensionControl [#setextensioncontrol-2]

设置插件控制。

```cpp
virtual void setExtensionControl(IExtensionControl* control)
```

成功调用该方法后，你需要保存 SDK 在该方法中返回的 `IExtensionControl` 对象。该对象用于触发事件或日志，实现插件与 App 之间的交互。例如，如果你调用了 `IExtensionControl` 中的 `fireEvent` 方法：

```cpp
void ByteDanceProcessor::dataCallback(const char* data){
    if (control_ != nullptr) {
        control_->fireEvent(id_, "beauty", data);
    }
}
```

App 层在初始化 `AgoraRtcEngineKit` 时注册的 `AgoraMediaFilterEventDelegate` 实例会收到该信息，并在 App 层触发如下回调：

```cpp
- (void)onEvent:(NSString *)provider extension:(NSString *)extension key:(NSString *)key value:(NSString *)value {
// 其中的 vendor 为注册插件时的 VENDOR_NAME，key/value 是插件消息的键值对
...
}
```

#### enumerateExtensions [#enumerateextensions-2]

提供所有支持封装的插件的信息。SDK 在加载插件时，会调用该方法向插件发送回调。收到该回调后，你需要通过返回值提供所有支持封装的插件的信息。

```cpp
virtual void enumerateExtensions(ExtensionMetaInfo* extension_list,
                                 int& extension_count) {
    (void) extension_list;
    extension_count = 0;
}
```

| 参数 | 描述 |
| --- | --- |
| `extension_list` | [audio-filter-android-ios-macos-windows-html-1-1-1 details](#table-slot-audio-filter-android-ios-macos-windows-html-1-1-1-2) |
| `extension_count` | 支持封装的插件的总数量。 |

<a id="table-slot-audio-filter-android-ios-macos-windows-html-1-1-1-2"></a>

**audio-filter-android-ios-macos-windows-html-1-1-1 details**

插件的信息，包括插件类型和插件名称。

支持的音频插件类型有：

- `AUDIO_RECORDING_LOCAL_PLAYBACK_FILTER`（上行）
- `AUDIO_POST_PROCESSING_FILTER`（上行）
- `AUDIO_REMOTE_USER_PLAYBACK_FILTER`（上行）

插件名由插件服务商指定，需要在 `IExtensionProvider` 对象内唯一。插件名为 String 型，支持如下字符：

- 26 个小写字母 `a`~`z`
- 数字和下划线（不能是第一个或最后一个）
如果插件名不符合规则，会无法通过编译。

其中，插件的信息定义如下：

```cpp
// 插件类型指插件在音视频传输通道中的位置，分类如下：
enum EXTENSION_TYPE {
    // 已废弃，请改用 AUDIO_RECORDING_LOCAL_PLAYBACK_FILTER 或 AUDIO_POST_PROCESSING_FILTER
    AUDIO_FILTER,
    // 视频前处理插件
    VIDEO_PRE_PROCESSING_FILTER,
    // 视频后处理插件
    VIDEO_POST_PROCESSING_FILTER,
    // 预留参数
    AUDIO_SINK,
    // 预留参数
    VIDEO_SINK,
    // 用于处理本地采集（如耳返采集）的，用于本地播放的音频数据
    AUDIO_RECORDING_LOCAL_PLAYBACK_FILTER = 10000,
    // 经过 3A 算法后的本地音频数据后处理
    AUDIO_POST_PROCESSING_FILTER = 10001,
    // 用于对远端用户的音频数据进行处理
    AUDIO_REMOTE_USER_PLAYBACK_FILTER = 10002,
};

// 插件的信息，包括插件类型和插件名称
struct ExtensionMetaInfo {
    EXTENSION_TYPE type;
    const char* extension_name;
};
```

如果你在 `enumerateExtensions` 方法中返回的插件类型为 `AUDIO_FILTER`，则在 App 开发者初始化 `RtcEngine` 并创建 `IExtensionProvider` 对象后，SDK 会调用 `createAudioFilter` 方法。

#### createAudioFilter [#createaudiofilter-2]

创建音频插件。SDK 调用该方法后，你需要返回 `IAudioFilter` 实例。

```cpp
virtual agora_refptr<IAudioFilter> createAudioFilter()
```

成功创建 `IAudioFilter` 实例后，音频插件会在合适的时机通过 `IAudioFilter` 类对输入的音频数据进行处理。

#### 示例代码 [#示例代码-5]

参考如下示例代码了解如何使用上述方法封装音频插件：

```cpp
void ExtensionProvider::enumerateExtensions(ExtensionMetaInfo* extension_list,
                                            int& extension_count) {
    extension_count = 1;
    ExtensionMetaInfo i;
    i.type = EXTENSION_TYPE::AUDIO_RECORDING_LOCAL_PLAYBACK_FILTER;
    i.extension_name = "YourExtensionName";
    extension_list[0] = i;
}

agora_refptr<agora::rtc::IAudioFilter> ExtensionAudioProvider::createAudioFilter() {
    PRINTF_ERROR("ExtensionAudioProvider::createAudioFilter");
    auto audioFilter = new agora::RefCountedObject<agora::extension::ExtensionAudioFilter>(audioProcessor_);
    return audioFilter;
}

void ExtensionAudioProvider::setExtensionControl(rtc::IExtensionControl* control){
    audioProcessor_->setExtensionControl(control);
}
```

## 打包音频插件 [#打包音频插件-2]

完成插件开发后，你需要对其进行注册、打包，并将最终的 `.framework` 或 `.xcframework` 文件，连同一个包含了插件名称、服务商名称和 Filter 名称的文件提交给声网进行验证。

插件通过宏 `REGISTER_AGORA_EXTENSION_PROVIDER` 进行注册，该宏位于 `AgoraExtensionProviderEntry.h` 文件中。

你需要在插件的入口使用这个宏。SDK 在加载插件时，该宏会自动向 SDK 注册你的插件。注意填入 `PROVIDER_NAME` 时不要填写标点符号。示例：

```cpp
REGISTER_AGORA_EXTENSION_PROVIDER(ByteDance, agora::extension::ExtensionProvider);
```
<_PlatformProcessedMarker close="true" />

<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="web" />
本文展示如何开发一个与声网 Web SDK 4.x 搭配使用的音频插件。

## 技术原理 [#技术原理]

声网为开发音频插件提供以下抽象类：

- 必需的基础类：
  - `AudioExtension`：实现插件的初始化，包括创建插件、设置日志上报、设置事件上报等。
  - `AudioProcessor`：实现音频数据的处理能力，包括接收、处理和返回处理完的音频数据。
- 可选的辅助类：
  - `Ticker`：帮助管理周期性任务。
  - `Logger`：向声网 SDK 上传日志。
  - `Reporter`：向声网 SDK 报告事件。

## 前提条件 [#前提条件-3]

开发前，请确保你的开发环境满足以下要求：

- Windows 或 macOS 计算机，需满足以下要求：
  - 下载声网 Web SDK [支持的浏览器](/zh-CN/realtime-media/rtc/reference/browser-compatibility)。声网强烈推荐使用[最新稳定版 Google Chrome 浏览器](https://www.google.com/chrome/)。
  - 具备物理音视频采集设备。
  - 可连接到互联网。如果你的网络环境部署了防火墙，请参考[应对防火墙限制](/zh-CN/realtime-media/rtc/build/setup-and-access/firewall)以正常使用声网服务。
  - 搭载 2.2 GHz Intel 第二代 i3/i5/i7 处理器或同等性能的其他处理器。
- 安装 [Node.js 及 npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)。

## 准备开发环境 [#准备开发环境-3]

通过 npm 将插件开发模块（[agora-rte-extension](https://www.npmjs.com/package/agora-rte-extension)）集成到你的项目中：

1. 运行以下命令安装：

   ```shell
   npm install --save agora-rte-extension
   ```

2. 在你的 `.js` 文件中加入以下代码导入插件开发模块：

   ```javascript
   import {AudioExtension, AudioProcessor} from 'agora-rte-extension'
   ```

## 开发音频插件 [#开发音频插件-3]

本节详细介绍开发音频插件必须实现的 API。你还可以根据实际需要实现一些辅助类 API，优化插件的设计和性能，详见[ API 参考](#ref)。

### 实现插件初始化 [#实现插件初始化]

插件初始化通过 `AudioExtension` 类实现。你需要实现 `AudioExtension` 类的 `createProcessor` 方法：

#### createProcessor [#createprocessor]

```typescript
abstract class AudioExtension<T extends AudioProcessor> {
    createProcessor(): T;
}
```

创建  `AudioProcessor`  实例。

插件使用者调用 `extension.createProcessor` 时，SDK 会调用该方法。你需要在该方法中返回创建好的 `AudioProcessor` 实例。

### 实现音频数据处理 [#实现音频数据处理]

实现音频数据处理包括以下步骤：

1. 接收待处理的音频：通过 `AudioProcessor` 类的 `onNode` 方法和 `IAudioProcessorContext` 接口的 `getAudioContext` 方法实现。
2. 处理音频：需要你自行实现。
3. 输出处理后的音频：通过 `AudioProcessor` 类的 `output` 方法实现。

#### onNode [#onnode]

```typescript
abstract onNode?(node: AudioNode, context: IAudioProcessorContext): void;
```

从前一个节点接收到待处理的音频数据回调。

**参数**

- `node`：前一个  `AudioNode`。`AudioNode` 是 Web Audio API 提供的接口类，详见 [Web Audio API interfaces](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API#web_audio_api_interfaces)。

- `context`：当前音频处理管道的 context。

#### output [#output]

```typescript
output(track: MediaStreamTrack | AudioNode, context: IProcessorContext): void;
```

输出处理后的音频数据。

**参数**

- `track`：处理后的音频数据。推荐使用  `AudioNode`。

- `context`：当前音频处理管道的 context。

#### getAudioContext [#getaudiocontext]

```typescript
getAudioContext(): Promise<AudioContext>;
```

获取当前音频处理管道的 `AudioContext`。`AudioContext` 是 Web Audio API 提供的接口类，详见 [Web Audio API interfaces](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API#web_audio_api_interfaces)。

### 示例项目 [#示例项目-2]

声网提供一个 Web 插件开发的示例项目供你参考，你可以前往下载或查看其中的源代码。

- Gitee：[ExtensionDemo](https://gitee.com/agoraio-community/API-Examples-Web/tree/main/ExtensionDemo)
- GitHub：[ExtensionDemo](https://github.com/Shengwang-Lab/API-Examples-Web/tree/main/ExtensionDemo)

以下示例代码展示了如何开发一个音频插件：

```typescript
class YourExtension extends AudioExtension<YourProcessor> {
    // 创建 Processor
    protected _createProcessor(): YourProcessor {
        return new YourProcessor();
    }
}

class CustomAudioProcessor extends AudioProcessor {
    // 从前一个 AudioNode 接收到音频数据
    onNode(node: AudioNode, context: IAudioProcessorContext) {
        // 创建 AudioNode
        const audioContext = context.getAudioContext();
        const gainNode = audioContext.createGain();

        // 连接前一个 AudioNode
        node.connect(gainNode);
    }

    process() {
        // TODO：添加你的插件的音频处理逻辑

        // 输出处理后的音频数据
        this.output(gainNode, context);
    }
}
```

<a id="ref"></a>
## API 参考 [#api-参考]
本节介绍开发音频插件的辅助类 API。

### AudioProcessor [#audioprocessor]

#### name [#name]

```typescript
name: string;
```

`Processor` 的名称。

#### enabled [#enabled]

```typescript
enabled :boolean;
```

`Processor` 的开启状态。

#### ID [#id]

```typescript
public readonly ID:string;
```

`Processor` 的标识符。

#### Kind [#kind]

```typescript
public get Kind():'video' | 'audio';
```

`Processor` 的类型，分为视频和音频两种。

#### context [#context]

```typescript
protected context?: IProcessorContext;
```

用于 `Processor` 请求原始媒体流并重新进行采集。

#### onPiped [#onpiped]

```typescript
abstract onPiped?(context: IProcessorContext): void;
```

`LocalVideoTrack` 连接到当前媒体处理管道的回调。

:::warning[注意]
如果只是连接多个 `Processor`，不会触发该回调。例如：插件使用者调用 `processorA.pipe(processorB)`。
:::

#### onUnPiped [#onunpiped]

```typescript
abstract onUnPiped?(): void;
```

`Processor` 断开与媒体处理管道的连接回调。

#### onEnableChange [#onenablechange]

```typescript
abstract onEnableChange?(enabled: boolean): void | Promise<void>;
```

`Processor` 的开启状态变化回调。

#### inputNode [#inputnode]

```typescript
protected inputNode?:AudioNode;
```

`Processor` 接收到的音频。

#### outputNode [#outputnode]

```typescript
protected outputNode?:AudioNode;
```

`Processor` 处理后的音频。

### IAudioProcessorContext [#iaudioprocessorcontext]

```typescript
interface IAudioProcessorContext extends IProcessorContext {
    getAudioContext(): Promise<AudioContext>;
}
```

用于 `AudioProcessor` 获取当前音频处理管道的 `AudioContext`。

### Ticker [#ticker]

#### constructor [#constructor]

```typescript
public constructor(type:"Timer" | "RAF" | "Oscillator", interval: number):Ticker;
```

`Ticker` 的构造函数。

**参数**

- `type`：类型，支持以下三种：

  - `Timer`: 使用 `setTimeout` 作为插件内部的计时器。

  - （推荐）`RAF`: 使用 `requestAnimationFrame` 作为插件内部的计时器。大多数情况下，该类型的  `Ticker` 渲染性能最好。

  - `Oscillator`: 使用 `WebAudio` 的 `OscillatorNode` 作为插件内部的计时器。该类型的  `Ticker` 在浏览器页面不被浏览时也能运行。

- `interval`：两次回调之间的间隔。 `Ticker` 会按照这个间隔执行，但无法保证 100% 精准。

#### Ticker.add [#tickeradd]

```typescript
public add(fn: Function): void;
```

添加计时任务。

#### Ticker.remove [#tickerremove]

```typescript
public remove():void;
```

取消添加的计时任务。

#### Ticker.start [#tickerstart]

```typescript
public start():void;
```

开始计时。

#### Ticker.stop [#tickerstop]

```typescript
public stop():void;
```

停止计时。

### Logger [#logger]

```typescript
interface IExtensionLogger {
    debug(...args: any): void;
    error(...args: any): void;
    info(...args: any): void;
    warning(...args: any): void;
}
```

提供四种级别的日志。

插件使用者如果在调用 `AgoraRTC.registerExtension` 注册插件时选择了上传日志，则通过 `Logger` 类上报的日志会被上传。

### Reporter [#reporter]

#### Reporter.reportApiInvoke [#reporterreportapiinvoke]

```typescript
public reportApiInvoke<T>(params: ReportApiInvokeParams): AgoraApiExecutor<T>;
```

向 SDK 报告 API 调用的相关事件。

其中 `ReportApiInvokeParams` 和 `AgoraApiExecutor` 的定义如下：

```typescript
interface ReportApiInvokeParams {
    // 被调用的 API 的名称
    name: string;
    // 与该 API 有关的参数或选项
    options: any;
    // 是否报告该 API 的调用结果
    reportResult?: boolean;
    // 定义多久为调用超时
    timeout?: number;
}
interface AgoraApiExecutor<T> {
    // API 调用成功
    onSuccess: (result: T) => void;
    // API 调用失败
    onError: (err: Error) => void;
}

```
<_PlatformProcessedMarker close="true" />

<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="windows" />
本文展示如何使用声网 SDK 提供的 API 开发一个音频插件。

开发音频插件需要使用以下接口：

- `IAudioFilter`：实现音频数据的处理能力，包括接收、处理和返回处理完的音频帧。
- `IExtensionProvider`：将音频数据的处理能力封装为插件。

## 前提条件 [#前提条件-4]

开发前，请确保你的开发环境满足以下要求：

- Microsoft Visual Studio 2017 或以上版本。
- Windows 7 或以上版本的设备。

## 准备开发环境 [#准备开发环境-4]

参考如下步骤将声网云市场的 API 添加到你的项目中：

1. 下载[视频 SDK v4.x](/zh-CN/realtime-media/rtc/reference/downloads)，然后解压下载的 SDK 包。
2. 将 SDK 包中的 `/rtc/sdk/low_level_api/include` 路径下的头文件拷贝至你的项目文件夹中。

## 开发音频插件 [#开发音频插件-4]

### 实现插件 [#实现插件-3]

音频插件通过 `IAudioFilter` 接口实现。该接口位于 `NGIAgoraMediaNode.h` 文件中。你需要实现 `IAudioFilter` 接口类的如下方法：

- [`adaptAudioFrame`](#adaptaudioframe-3)
- [`setEnabled`](#setenabled-3)
- [`isEnabled`](#isenabled-3)
- [`setProperty`](#setproperty-3)
- [`getProperty`](#getproperty-3)
- [`getName`](#getname-3)
- [`getPreferredSampleRate`](#getpreferredsamplerate-3)（可选）
- [`getPreferredChannelNumbers`](#getpreferredchannelnumbers-3)（可选）

#### adaptAudioFrame [#adaptaudioframe-3]

处理音频帧。该方法是 `IAudioFilter` 类的核心方法。调用该方法后，SDK 处理 `inAudioFrame` 中的音频数据，然后通过 `adaptedFrame` 返回处理后的数据。该方法目前仅支持输入输出 PCM 格式的音频数据。

```cpp
virtual bool adaptAudioFrame(const media::base::AudioPcmFrame& inAudioFrame,
                             media::base::AudioPcmFrame& adaptedFrame) = 0;
```

| 参数               | 描述                                                         |
| :----------------- | :----------------------------------------------------------- |
| `inAudioFrame`               | 输入参数。待处理的音频帧。 |
| `adaptedFrame` | 输出参数。处理后的音频帧。 |

#### setEnabled [#setenabled-3]

启用或关闭音频插件。

```cpp
virtual void setEnabled(bool enable) {}
```

| 参数 | 描述 |
| --- | --- |
| `enable` | [audio-filter-android-ios-macos-windows-html-0-1-1 details](#table-slot-audio-filter-android-ios-macos-windows-html-0-1-1-3) |

<a id="table-slot-audio-filter-android-ios-macos-windows-html-0-1-1-3"></a>

**audio-filter-android-ios-macos-windows-html-0-1-1 details**

是否启用音频插件：

- true: 启用音频插件。
- false: (默认) 关闭音频插件。

#### isEnabled [#isenabled-3]

检查音频插件是否已启用。

```cpp
virtual bool isEnabled() { return true; }
```

**返回值**

是否启用音频插件：

- true: 启用音频插件。
- false: (默认) 关闭音频插件。

#### setProperty [#setproperty-3]

设置音频插件属性。App 开发者调用 `setExtensionProperty` 时，SDK 会调用该方法。你需要返回音频插件的属性。

```cpp
size_t setProperty(const char* key, const void* buf, size_t buf_size)
```

| 参数     | 描述                                                         |
| :------- | :----------------------------------------------------------- |
| `key`      | 插件属性的 `key`。                                             |
| `buf`      | 插件属性 `key` 值对应的 buffer 地址，数据形式为 JSON 字符串。我们推荐使用第三方的 `nlohmann/json library` 开源库，帮助实现 C++ 的 struct 和 JSON 字符串之前的序列和反序列化。 |
| `buf_size` | 插件属性 buffer 的内存大小。                                 |

#### getProperty [#getproperty-3]

获取音频插件属性。App 开发者调用 `getProperty` 时，SDK 会调用该方法获取音频插件的属性。

```cpp
size_t getProperty(const char* key, char* property, size_t buf_size)
```

| 参数     | 描述                         |
| :------- | :--------------------------- |
| `key`      | 插件属性的 `key`。             |
| `property` | 插件属性指针。               |
| `buf_size` | 插件属性 buffer 的内存大小。 |

#### getName [#getname-3]

查询插件的名称。你需要在该方法中返回使用的音频插件的名称。

```cpp
virtual const char * getName() const = 0;
```

#### getPreferredSampleRate [#getpreferredsamplerate-3]

查询音频插件需要的音频采样率。

该方法为可选。如果你在该方法中返回一个指定的采样率，SDK 会将音频数据重采样成指定的采样率，然后传入音频插件。

```cpp
virtual int getPreferredSampleRate()  { return 0; };
```

#### getPreferredChannelNumbers [#getpreferredchannelnumbers-3]

查询音频插件需要的音频声道数。

该方法为可选。如果你在该方法中返回一个指定的声道数，SDK 会将音频数据重采样成指定的声道数，然后传入音频插件。

```cpp
virtual int getPreferredChannelNumbers()  { return 0; };
```

#### 示例代码 [#示例代码-6]

参考如下示例代码了解如何使用上述方法实现一个音频插件：

```cpp
// 在收到需要处理的音频数据后，调用 adaptAudioFrame 方法，对收到的音频数据进行处理。
bool ExtensionAudioFilter::adaptAudioFrame(const media::base::AudioPcmFrame& inAudioPcmFrame,
                                           media::base::AudioPcmFrame& adaptedPcmFrame) {
    return audioProcess_->processFrame(inAudioPcmFrame, adaptedPcmFrame) == 0;
}

// 调用 setProperty 设置音频插件参数。
int ExtensionAudioFilter::setProperty(const char* key, const void* buf, int buf_size) {
    std::string str_volume = "100";
    if (std::string(key) == "volume") {
        str_volume = std::string(static_cast<const char*>(buf), buf_size);
    }

    int int volume_ = atoi(str_volume.c_str());
    audioProcessor_->setVolume(int_volume_);
    return ERR_OK;
}

// 调用 getProperty 查询音频插件参数。
int ExtensionAudioFilter::getProperty(const char* key, void* buf, int buf_size) const override {return ERR_OK; }

// 调用 setEnabled 开启音频插件。
void ExtensionAudioFilter::setEnabled(bool enable) override { enabled_ = enable; }

// 调用 isEnabled 查询音频插件开启状态。
bool ExtensionAudioFilter::isEnabled() const override {return enabled_; }

// 在 getName 的返回值中传入插件名称。
const char* ExtensionAudioFilter::getName() const override { return "YourExtensionName"; }

// （可选）在 getPreferredSampleRate 的返回值中指定音频插件需要的音频采样率。
const char* ExtensionAudioFilter::getPreferredSampleRate() override { return 48000; }

// （可选）在 getPreferredChannelNumbers 的返回值中指定音频插件需要的音频声道数。
int ExtensionAudioFilter::getPreferredChannelNumbers() override { return 2; };
```

### 封装插件 [#封装插件-3]

封装插件通过 `IExtensionProvider` 接口类实现。该接口类位于 `NGIAgoraExtensionProvider.h` 文件中。你需要先实现这个接口类，并实现如下方法：

- [`setExtensionControl`](#setextensioncontrol-3)
- [`enumerateExtensions`](#enumerateextensions-3)
- [`createAudioFilter`](#createaudiofilter-3)

`IExtensionProvider` 是插件动态库的提供者，由全局唯一的名字标识。每一个插件动态库可以提供一个或多个有不同功能的插件，如音频前处理插件库可以提供不同功能的音频插件（Audio filter）。每个插件是一个具体的 Extension 对象。

#### setExtensionControl [#setextensioncontrol-3]

设置插件控制。

```cpp
virtual void setExtensionControl(IExtensionControl* control)
```

成功调用该方法后，你需要保存 SDK 在该方法中返回的 `IExtensionControl` 对象。该对象用于触发事件或日志，实现插件与 App 之间的交互。例如，如果你调用了 `IExtensionControl` 中的 `fireEvent` 方法：

```cpp
void ByteDanceProcessor::dataCallback(const char* data){
    if (control_ != nullptr) {
        control_->fireEvent(id_, "beauty", data);
    }
}
```

App 层在初始化 `RtcEngine` 时注册的 `IMediaExtensionObserver` 实例会收到该信息，并在 App 层触发如下回调：

```cpp
@Override
public void onEvent(String vendor, String key, String value) {
// 其中的 vendor 为注册插件时的 VENDOR_NAME，key/value 是插件消息的键值对
...
}
```

#### enumerateExtensions [#enumerateextensions-3]

提供所有支持封装的插件的信息。SDK 在加载插件时，会调用该方法向插件发送回调。收到该回调后，你需要通过返回值提供所有支持封装的插件的信息。

```cpp
virtual void enumerateExtensions(ExtensionMetaInfo* extension_list,
                                 int& extension_count) {
    (void) extension_list;
    extension_count = 0;
}
```

| 参数 | 描述 |
| --- | --- |
| `extension_list` | [audio-filter-android-ios-macos-windows-html-1-1-1 details](#table-slot-audio-filter-android-ios-macos-windows-html-1-1-1-3) |
| `extension_count` | 支持封装的插件的总数量。 |

<a id="table-slot-audio-filter-android-ios-macos-windows-html-1-1-1-3"></a>

**audio-filter-android-ios-macos-windows-html-1-1-1 details**

插件的信息，包括插件类型和插件名称。

支持的音频插件类型有：

- `AUDIO_RECORDING_LOCAL_PLAYBACK_FILTER`（上行）
- `AUDIO_POST_PROCESSING_FILTER`（上行）
- `AUDIO_REMOTE_USER_PLAYBACK_FILTER`（上行）

插件名由插件服务商指定，需要在 `IExtensionProvider` 对象内唯一。插件名为 String 型，支持如下字符：

- 26 个小写字母 `a`~`z`
- 数字和下划线（不能是第一个或最后一个）
如果插件名不符合规则，会无法通过编译。

其中，插件的信息定义如下：

```cpp
// 插件类型指插件在音视频传输通道中的位置，分类如下：
enum EXTENSION_TYPE {
    // 已废弃，请改用 AUDIO_RECORDING_LOCAL_PLAYBACK_FILTER 或 AUDIO_POST_PROCESSING_FILTER
    AUDIO_FILTER,
    // 视频前处理插件
    VIDEO_PRE_PROCESSING_FILTER,
    // 视频后处理插件
    VIDEO_POST_PROCESSING_FILTER,
    // 预留参数
    AUDIO_SINK,
    // 预留参数
    VIDEO_SINK,
    // 用于处理本地采集（如耳返采集）的，用于本地播放的音频数据
    AUDIO_RECORDING_LOCAL_PLAYBACK_FILTER = 10000,
    // 经过 3A 算法后的本地音频数据后处理
    AUDIO_POST_PROCESSING_FILTER = 10001,
    // 用于对远端用户的音频数据进行处理
    AUDIO_REMOTE_USER_PLAYBACK_FILTER = 10002,
};

// 插件的信息，包括插件类型和插件名称
struct ExtensionMetaInfo {
    EXTENSION_TYPE type;
    const char* extension_name;
};
```

如果你在 `enumerateExtensions` 方法中返回的插件类型为 `AUDIO_FILTER`，则在 App 开发者初始化 `RtcEngine` 并创建 `IExtensionProvider` 对象后，SDK 会调用 `createAudioFilter` 方法。

#### createAudioFilter [#createaudiofilter-3]

创建音频插件。SDK 调用该方法后，你需要返回 `IAudioFilter` 实例。

```cpp
virtual agora_refptr<IAudioFilter> createAudioFilter()
```

成功创建 `IAudioFilter` 实例后，音频插件会在合适的时机通过 `IAudioFilter` 类对输入的音频数据进行处理。

#### 示例代码 [#示例代码-7]

参考如下示例代码了解如何使用上述方法封装音频插件：

```cpp
void ExtensionProvider::enumerateExtensions(ExtensionMetaInfo* extension_list,
                                            int& extension_count) {
    extension_count = 1;
    ExtensionMetaInfo i;
    i.type = EXTENSION_TYPE::AUDIO_RECORDING_LOCAL_PLAYBACK_FILTER;
    i.extension_name = "YourExtensionName";
    extension_list[0] = i;
}

agora_refptr<agora::rtc::IAudioFilter> ExtensionAudioProvider::createAudioFilter() {
    PRINTF_ERROR("ExtensionAudioProvider::createAudioFilter");
    auto audioFilter = new agora::RefCountedObject<agora::extension::ExtensionAudioFilter>(audioProcessor_);
    return audioFilter;
}

void ExtensionAudioProvider::setExtensionControl(rtc::IExtensionControl* control){
    audioProcessor_->setExtensionControl(control);
}
```

## 打包音频插件 [#打包音频插件-3]

完成插件开发后，你需要对其进行注册、打包，并将最终的 `.dll` 文件，连同一个包含了插件名称、服务商名称和 Filter 名称的文件提交给声网进行验证。

### 注册插件 [#注册插件-1]

插件通过宏 `REGISTER_AGORA_EXTENSION_PROVIDER` 进行注册，该宏位于 `AgoraExtensionProviderEntry.h` 文件中。

你需要在插件的入口使用这个宏。SDK 在加载插件时，该宏会自动向 SDK 注册你的插件。注意填入 `PROVIDER_NAME` 时不要填写标点符号。示例：

```cpp
REGISTER_AGORA_EXTENSION_PROVIDER(ByteDance, agora::extension::ExtensionProvider);
```

### 提供插件信息 [#提供插件信息-1]

新建一个 `.md` 文件，并在其中填入如下信息：

- `EXTENSION_NAME`：插件包的名称，如 `libagora-bytedance.dll` 文件的 `EXTENSION_NAME` 就是 `agora-bytedance`。
- `EXTENSION_VENDOR_NAME`：服务商名称，即插件的 `Provider` 对象名称，用于在 `.cpp` 文件中进行注册。
- `EXTENSION_FILTER_NAME`：`ExtensionProvider.h` 文件中定义的 Filter 名称。
<_PlatformProcessedMarker close="true" />
</_PlatformTabsGroup>
