dtln-aecを用いたダブルトークキャンセル同時翻訳

時雨堂さんが提供してくれているdtln-aecを用いて、ダブルトークキャンセル同時通訳を作ってみました。
ライブラリが要求する音声形式が結構シビアなのでちょっと苦労しました。

![DTLN.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4114668/a534fd6b-1503-4fb6-bfd9-c92ccf8bb393.jpeg)

マイク入力した日本語を英語に翻訳し、スピーカーから音声合成した英語が流れるものです。
かなり爆音でスピーカーから合成音声を流していても、その最中に喋った声もちゃんと音声認識してくれます。

これでパソコン以外でも動作してくれたら完璧なんですが、MediaStreamTrackProcessor等の最新APIに対応している必要があるため、スマホでは動作しません。
ブラウザも基本的にはChrome系限定です。
ただネイティブアプリで組めば本元のdtln-aecが使えるのでスマホでも実現可能ではあります。
CPU負荷はそれなりに大きいので、小さめのモデルを使う必要があるかもしれませんが。

デモは翻訳ですが、受付システムや同室内でのビデオ会議のハウリング対策などにも応用可能かと思います。

---

## 主な処理フローと技術ポイント

### 1. マイク入力→dtln-aec→Azure音声認識

- **MediaStreamTrackProcessor**でマイクの音声ストリームをAudioData単位で取得
- **dtln-aec**の`processInputAudioData`にAudioDataを渡し、エコー除去・ノイズ抑制
  - このメソッドはAudioData（WebCodecs APIオブジェクト）型かつモノラル（1ch）でのみ動作。Int16ArrayやFloat32Arrayなどの生配列では直接渡せません
  - データがステレオの場合はモノラル化(このソースでは左chだけ抽出)してAudioDataを再生成する必要あり
  - dtln-aecは128サンプル単位で処理する前提なので、MediaStreamTrackProcessorも128フレームごとにAudioDataを生成(512モデルを使う意義はやや不明)
- dtln-aec処理済みAudioDataをMediaStreamTrackGenerator経由でストリーム化し、Azure Speech SDKの`AudioConfig.fromStreamInput()`に渡す

```javascript
processor.readable
  .pipeThrough(new TransformStream({
    transform(audioData, controller) {
      let processedList = [audioData];
      if (dtlnAec && document.getElementById("dtlnToggle").checked) {
        try {
          processedList = dtlnAec.processInputAudioData(audioData);
        } catch (e) {
          console.error("dtln-aec推論エラー", e);
        }
      }
      for (const processed of processedList) {
        controller.enqueue(processed);
      }
    }
  }))
  .pipeTo(generator.writable);
```

---

### 2. 音声認識→翻訳→音声合成

- 音声認識結果をMicrosoft Translator APIで英語に翻訳
- 翻訳文をAzure Speech SDKで音声合成し、16bit/16kHzモノラルPCMデータとして取得
- PCMデータにWAVヘッダーを付加し、Blob化

```javascript
function createWavBlob(pcmBuffer, sampleRate = 16000, numChannels = 1, bitsPerSample = 16) {
  // ...WAVヘッダー付与処理...
  return new Blob([wavBytes], { type: 'audio/wav' });
}
```

---

### 3. 音声合成→WAV化→Audioタグ→dtln-aecパイプライン

- 合成音声をWAV化し、Audioタグで再生
- Web Audio APIのAudioContext/MediaStreamDestination/MediaElementSourceで再生信号をMediaStream化
- MediaStreamTrackProcessorでAudioDataとして取得し、dtln-aecの`processOutputAudioData`に渡す
  - ステレオの場合はモノラル化(このケースではLchだけ抽出)してAudioDataを作り直す必要あり
  - dtln-aecはAudioData型かつモノラルのみ受け付ける
- ここでも128サンプル単位でAudioDataが生成される

```javascript
outputAudioProcessor.readable
  .pipeThrough(new TransformStream({
    transform: (data, controller) => {
      let dataForDtln = data;
      if (data.numberOfChannels > 1) {
        const lch = new Float32Array(data.numberOfFrames);
        data.copyTo(lch, { planeIndex: 0 });
        dataForDtln = new AudioData({
          format: "f32",
          sampleRate: data.sampleRate,
          numberOfFrames: data.numberOfFrames,
          numberOfChannels: 1,
          timestamp: data.timestamp,
          data: lch.buffer
        });
      }
      if (dtlnAec && document.getElementById("dtlnToggle").checked) {
        dtlnAec.processOutputAudioData(dataForDtln);
      }
      controller.enqueue(data);
    }
  }), { signal })
```
- AudioData以外（Int16ArrayやFloat32Arrayなど）ではdtln-aecはエラーになるので注意
---

## 注意点・工夫

- **AudioData型**：時雨堂さん提供のWeb版のdtln-aecはAudioData型のみ受け付け、モノラルでないとエラー。Int16ArrayやFloat32Arrayでは不可
- **フレームサイズ**：MediaStreamTrackProcessor/Generatorは128サンプル単位でデータをやりとりするため、dtln-aecの内部処理と一致
- **サンプルレート**：16kHzを基本とし、必要に応じてリサンプリング
---

## まとめ

- dtln-aecによるリアルタイムエコー除去とAzure音声サービスを組み合わせ、ブラウザのみで音声認識→翻訳→音声合成→再生まで一気通貫で体験できる
- すべての音声処理はAudioData単位・128サンプル単位で行い、dtln-aecの仕様に合わせてモノラル変換も徹底

---

**参考：主なライブラリ・API**
- TensorFlow.js
- Shiguredo.DtlnAec (https://github.com/shiguredo/dtln-aec)
- Microsoft Azure Speech SDK
- Microsoft Translator API
- MediaStreamTrackProcessor/Generator（WebCodecs API）

---