【新機能レポート】Zoom Video SDK for Webで「音声ピッチシフト」してみた 〜Raw Data Processorの可能性〜

こんにちは🌱
今年に入って、Zoom Video SDK for Web のバージョン 2.1.5 から Raw Data Processor 機能が加わりました。
このアップデートで、いままで Web SDK でやりたかったことが、もっと身近にできるようになっています☺️

https://developers.zoom.us/changelog/video-sdk/web/2.1.5

今回は音声（オーディオ）ピッチシフトを例に、「どんな拡張ができるのか？」を、実コードとGitHubサンプルを交えてご紹介します。（映像のリアルタイム加工も、同じ仕組みで実装可能です。新しい可能性が広がりますね✨）

# できるようになったこと🪄
- オーディオ🎤：リアルタイムで声のピッチを変えたり、ノイズを除去したり、さまざまなエフェクトをかけることができます
- ビデオ🎥：ウォーターマークや映像加工、モザイク、色味の補正も自由に行えます（公式サンプルはウォーターマークのみですが、自作エフェクトも追加できます）

これらがすべてWebブラウザだけで完結します💻

# 使い方の概要📝
カスタムProcessor用のJSファイルを用意します。クライアントJからcreateProcessor/addProcessorでストリームに組み込みます。

# Raw Dataって？カスタムプロセッサーって？🧐
## Raw Dataとは…
Zoom Video SDK セッション内の音声や映像を「圧縮される前の生データ（PCMや画像フレーム）」として直接扱える仕組みのことです。これにより、フレームごと・音サンプルごとに好きな処理を加えることができるようになりました。

## カスタムプロセッサーとは...
Zoom SDK から渡された Raw Data を自分の好きなロジックで加工するクラスのこと。
- `AudioProcessor`（音声）、`VideoProcessor`（映像）クラスを継承し
- `process()`メソッドで、毎フレーム`inputs`からデータを受け取って`outputs`に書き出します

実際の動作イメージはWebAudioのAudioWorkletProcessorに近いですが、Zoom SDK独自のサンドボックス実行環境で安全・安定に分離されています🍀

# シンプルなピッチシフトのサンプル🎙️
まずは初心者向けの最小サンプルとして、VanillaなJSで完結したカスタムピッチシフターを紹介します💪

```javascript
class PitchShiftProcessor extends AudioProcessor {
  constructor(port, options) {
    super(port, options);
    //音声データを一時的に保存するリングバッファ（循環バッファ）のサイズ
    //44100Hzの場合11025サンプルは約0.25秒ぶん
    this.bufferSize = 11025;
    //バッファサイズ分だけの実際の音声データ記憶用配列（型はFloat32の配列）
    this.buffer = new Float32Array(this.bufferSize);
    //次に音声データを書き込むバッファの位置（インデックス）
    this.writePos = 0;
    //ピッチシフト処理時に読み出す現在のバッファ位置 小数点も含む（補間用）
    this.readPos = 0.0;
    //ピッチの倍率 1.0だと原音 大きいほど高い音に
    this.pitchRatio = options?.pitchRatio || 1.3;
    //フォルマント比（声の音色や響き）
    //ピッチだけ上げるとロボ声になりやすいがフォルマントも調整すると「自然な声質」に
    this.formantRatio = options?.formantRatio || 1.1;
    //0だとエフェクトなしの原音のみ、1.0だと完全にエフェクト音のみ
    this.dryWet = options?.dryWet || 0.7;
    //簡易ハイパスフィルタ alphaはフィルタ強度のパラメータ
    this.hpf = { prevIn: 0, prevOut: 0, alpha: 0.86 };
  }

  process(inputs, outputs) {
    const input = inputs[0];
    const output = outputs[0];
    if (input.length === 0 || !input[0]) return true;
    const inputChannel = input[0];
    const outputChannel = output[0];

    // write buffer
    for (let i = 0; i < inputChannel.length; i++) {
      this.buffer[this.writePos] = inputChannel[i];
      this.writePos = (this.writePos + 1) % this.bufferSize;
    }
    // pitch shift process
    for (let i = 0; i < outputChannel.length; i++) {
      let readPos = this.readPos % this.bufferSize;
      if (readPos < 0) readPos += this.bufferSize;
      const intPos = Math.floor(readPos);
      const frac = readPos - intPos;
      const nextPos = (intPos + 1) % this.bufferSize;
      const raw = this.buffer[intPos] * (1 - frac) + this.buffer[nextPos] * frac;

      // simple HPF
      const filtered = raw - this.hpf.prevIn + this.hpf.alpha * this.hpf.prevOut;
      this.hpf.prevIn = raw;
      this.hpf.prevOut = filtered;

      outputChannel[i] = filtered * this.dryWet + raw * (1 - this.dryWet);

      this.readPos += this.pitchRatio;
      if (this.readPos >= this.bufferSize) {
        this.readPos -= this.bufferSize;
        this.writePos = 0;
      }
    }
    return true;
  }
}
registerProcessor('pitch-shift-processor', PitchShiftProcessor);
```

# Zoom SDKでProcessorを使う手順🫰🏼
1. 上記ファイルをpublic/pitch-shift-processor.jsとして配置
2. クライアントJSファイルなどから以下のように呼び出します

```javascript
const processorParams = {
  name: 'pitch-shift-processor',
  type: 'audio',
  url: window.location.origin + '/pitch-shift-processor.js',//パスを合わせる
  options: { pitchRatio: 1.3 }
};
const processor = await stream.createProcessor(processorParams);
await stream.addProcessor(processor);
```
これだけで、「Video SDK で話す声が、ピッチシフトされた状態」で他参加者に届きます😊

## Github サンプル
ここまでの内容を、こちらのサンプルでまとめましたのでよかったらCloneして試してみてください。ローカルでも試せます。（当然ですが、声を聞くには2拠点必要です😆）

https://github.com/bitzed/zoom-video-sdk-web-audioRawSample

# 応用編：importを使った本格的な開発手順
Video SDK Web の Raw Data Processor ではサンドボックス環境での実行が前提となっています。
そのため外部ライブラリ、例えば npm パッケージを使ってより本格的な加工などを行いたい場合、外部からの import ができないので、Webpack や Vite などを使って1ファイルにバンドルした上で読み込ませる必要があります。

## 構成例
```
my-zoom-processor-project/
├── package.json
├── vite.config.ts
├── processors/
│   └── pitch-shift-processor.ts
├── public/
│   └── pitch-shift-processor.js
├── src/
│   └── index.js
```

## ステップ
### 1. 初期化・必要なパッケージの導入
```bash
npm init -y
npm install vite @soundtouchjs/audio-worklet
```
### 2. TypeScript/JSでProcessorを実装（例: processors/pitch-shift-processor.ts）
```typescript
import { SoundTouch } from 'soundtouchjs';

class PitchShiftProcessor extends AudioProcessor {
  constructor(port, options) {
    super(port, options);
    this.soundTouch = new SoundTouch();
    // ...
  }
  process(inputs, outputs) {
    // SoundTouchで処理
  }
}
registerProcessor('pitch-shift-processor', PitchShiftProcessor);
```
### 3. Vite や Webpack でバンドル
```typescript
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
  build: {
    lib: {
      entry: 'processors/pitch-shift-processor.ts',
      formats: ['es'],
      fileName: () => 'pitch-shift-processor.js',
    },
    outDir: 'public',
    emptyOutDir: false,
    minify: false,
  },
});
```
```bash
npx vite build
```

## Zoom 公式サンプルのビデオ加工

Zoom 公式の Github では、より具体的なビデオの加工も紹介しています。

https://github.com/zoom/videosdk-web-processor-sample

このサンプルでは、@mediapipe/tasks-vision（Googleの顔検出AIライブラリ）をnpm importしてビルドし、「顔の位置を検出→ビデオストリームにマスクやウォーターマークを描画」など、映像の高度なリアルタイム加工も実装されています。

![Screenshot 2025-06-04 at 17.42.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/07254064-f9ae-4a50-8914-66f100fdaa0e.png)

`ZoomDualMaskVideoProcessor.ts` で
```typescript
import { FaceDetector, FilesetResolver } from '@mediapipe/tasks-vision';
```
のように、import を使って顔認識→canvas描画を行い、Viteで1ファイルにバンドルしてpublic配下でProcessorとして動かしています。

## 注意
- ProcessorのJSは最終的に「自己完結型1ファイル」にバンドルする必要があります
- 一部npmパッケージ（native依存やWebWorker系など）は動かないことがあるので注意です
- OSSを組み込む場合はライセンス表記も忘れずに

## まとめ
Zoom Video SDK for WebのRaw Data Processorのおかげで、Web会議でも音声・映像に自由な拡張やAI活用ができるようになりました✨
初心者の方も、npmパッケージを活用したい方も、この仕組みを使って新しいコミュニケーション体験を一緒に広げていきましょう。

困った時はお気軽にコメントやDMで相談してくださいね🍀