Amazon Web Services ブログ

Amazon Chime SDK for JavaScriptでオーディオやコンテンツを会議で共有する方法

バーチャル会議では参加者は通常の音声やビデオに加えて、リッチメディアを共有したいというお客様の要望を聞きます。Amazon Chime SDKは、アプリケーションやブラウザからのメディアを会議で共有することを可能にします。例えば、オンライン・フィットネス・クラスの音楽などです。オーディオやビデオはローカルコンピュータ、ローカルメディアファイル、ブラウザタブ、またはインターネット上の他のソースから共有できます。

しかし、音声やビデオのソースは必ずしも他の参加者と同条件で共有できる状態にあるとは限りません。音声の音量が大きすぎたり小さすぎたりして、リアルタイムに調整する必要があるかもしれません。また、ビデオやプレゼンテーションをトリミングしたり、回転させたりする必要があるかもしれません。

よくお客様から聞くのは講師やプレゼンターが話している間、あるいは翻訳者がライブで翻訳している間に、音楽やビデオのオーディオトラックの音量を一時的に下げることです。この作業をダッキングと呼びます。Amazon Chime SDK for JavaScriptでは、Webオーディオとビデオの変換パイプラインを利用して、これらの手順を表現するトランスフォームデバイスを定義することができます。

このブログ記事ではマイクの入力とコンテンツの共有音声に Webオーディオ トランスフォームを適用する方法を紹介します。さらに、このロジックをダッカーに拡張して、ユーザーが話している間にコンテンツ共有の音量を自動的に下げるために使用します。

前提条件

  • Building a Meeting Application using the Amazon Chime SDKに記載されているようにAmazon Chime SDKをアプリケーションに統合する方法の基本を理解していること。
  • meetingV2デモアプリケーションをお客様の開発ツールに組み込み、実行していること。
  • 1台のデバイスでテストする場合は、ヘッドフォンと音声出力の選択をサポートするブラウザ(Google Chromeなど)が必要です。

注:デモコードをデプロイして実行すると、AWSの料金が発生する場合があります。

ライセンス

このブログ記事に掲載されているコードは、amazon-chime-sdk-jsリポジトリの他のコードと同様に、Apache License 2.0の条件でライセンスされています。

はじめに

Amazon Chimeが他の参加者とオーディオを交換するために使用するマイク入力、オーディオエレメント出力、およびWebRTCレイヤーは、すべてウェブのmedia stream抽象化を利用して、アプリケーション内のオーディオの通過を定義されます。各ストリームは、オーディオまたはビデオの複数のトラックで構成されています。

A diagram showing how camera and microphone inputs are media streams consumed by the Amazon Chime SDK, sent via the WebRTC peer connection

Web Audioは入力ストリームを、audio context内のaudio nodeのグラフに接続するWeb技術です。これらのノードはオーディオトラックの変換や生成に使用できます。各ノードは少なくとも1つの入力または少なくとも1つの出力を持ち、多くの場合その両方を持ちます。オーディオノードについては、MDNのAudioNodeドキュメントで詳しく説明されています。Amazon Chime SDKは、ビデオの処理についても同様のコンセプトを提供しています。詳しくは、Video Processorのドキュメントをご覧ください。

A diagram showing how video pipeline processors and Web Audio nodes can be used to adjust camera and microphone input before the Amazon Chime SDK sends it via the WebRTC peer connection

Web Audio を使用して、マイク入力の音量を調整できる音声変換デバイスを定義し、MeetingV2 デモを使用して Web ブラウザでテストします。

同じ手法でデモを修正してコンテンツ共有音声の音量を調整し、デモの既存のリアルタイムボリュームオブザーバーにリンクして音量を自動調整します。

シンプルなオーディオトランスフォームの定義と使用

Amazon Chime SDK for JavaScriptでは、Web Audioノードをマイクの入力に適用するために必要な作業の多くが実装されています。AudioTransformDevice を実装した JavaScript クラスは、入力オーディオを変更する別のデバイスやオーディオノードのグラフの制約に対する変更を定義することができ、残りの処理はデバイスコントローラが行います。SingleNodeAudioTransformDevice抽象クラスを使用すれば、シンプルに1つのオーディオノードに対してトランスフォームを定義できます。

音量を調整する AudioTransformDevice を作成するにはGainNode を使用します。ゲインノードは1つのストリームを入力として受け取り、音量を調整して、1つのストリームを出力として放出します。オーディオトランスフォームデバイスは、GainNodeの設定を調整するメソッドを公開しています。アプリケーションコードは、マイクデバイスを使用する場合と同様に、DeviceController.chooseAudioInputDevice を使用して、新しいトランスフォームデバイスのインスタンスを直接選択できます。

新規ファイル meetingV2/audiotransform/VolumeTransformDevice.ts を作成し、以下のコードを記述します。

import {
  SingleNodeAudioTransformDevice,
} from 'amazon-chime-sdk-js';

export class VolumeTransformDevice extends SingleNodeAudioTransformDevice<GainNode> {
  private volume: number = 1.0; // So we can adjust volume prior to creating the node.

  async createSingleAudioNode(context: AudioContext): Promise<GainNode> {
    const node = context.createGain();

    // Start at whatever volume the user already picked for this device, whether
    // or not we were connected to the audio graph.
    node.gain.setValueAtTime(this.volume, context.currentTime);
    return node;
  }

  setVolume(volume: number): void {
    this.volume = volume;
    if (this.node) {
      this.node.gain.linearRampToValueAtTime(volume, this.node.context.currentTime + 0.25);
    }
  }
}
TypeScript

meetingV2.tsに上述のクラスをインポートして、デモアプリケーションで利用できます。

import { VolumeTransformDevice } from './audiotransform/VolumeTransformDevice';
TypeScript

selectAudioInputDeviceの上にコードの一部を追加します。

    // Let's interject volume control into the device selection process.
    if (!isAudioTransformDevice(device)) {
      const volumeTransformDevice = new VolumeTransformDevice(device);
      
      // Make `setVolume` visible to the page so we can change it!
      (window as any).setVolume = volumeTransformDevice.setVolume.bind(volumeTransformDevice);
      return this.selectAudioInputDevice(volumeTransformDevice);
    }
TypeScript

リビルドしてリローンチします。

cd demos/browser; npm run start
TypeScript

ヘッドホンをつけて、2つのブラウザウィンドウからテスト会議に参加します。「Web Audio」のチェックボックスにチェックを入れてください。

片方のウィンドウをミュートにして、もう片方のウィンドウでコンソールを開きます(Firefoxの場合は「ツール」→「Web開発者」→「Webコンソール」、Chromeの場合は「表示」→「開発者」→「JavaScriptコンソール」)。以下のように音量を調整します。話をするともう一方のウィンドウからの出力で音量が変化するのがわかります。

window.setVolume(0.1);
window.setVolume(0.5);
TypeScript

この例は、要点を説明しておりこのトランスフォームデバイス用のユーザーインターフェースをどのように構築するかの詳細はここでは説明しません。さらに詳細を見るために、HTMLに入力スライダーを追加することができます。

<input id="volume-in" type="range" min="0" max="1" step="0.1" value="1">
HTML

windowオブジェクトのsetVolume関数を格納する代わりに、入力の変更ハンドラにアタッチします。

document.getElementById('volume-in').onchange = setVolume;
TypeScript

アプリケーションにはVue、React、jQueryなどのフレームワークが使われていると思いますが、コンセプトは同じで、入力要素の変化を関連付けて、GainNodelinearRampToValueAtTimeを呼び出すまでの流れです。

コンテンツ共有の音量変更

Google Chromeをはじめとする一部のブラウザでは、タブ内で再生されている音声とそのビジュアルコンテンツを共有したり、コンピュータのスピーカーから再生されている音声とデスクトップ全体を共有したりすることができます。Amazon Chime SDKではコンテンツ共有を利用して、プレゼンテーション、ビデオ、音楽などを他の参加者と共有することができます。

マイクやカメラの入力と同様に、コンテンツ共有にはmedia streamを使用します。音声付きのコンテンツ共有の場合、ストリームには音声トラックとビデオトラックの両方が含まれます。

Amazon Chime SDKは、コンテンツ共有にAudioTransformDeviceトランスフォームを直接適用することをサポートしておらず、Web Audioノードはビデオトラックを通過しません。代わりに、AudioTransformDeviceのコードを翻訳してオーディオストリームに直接作用させ、結合されたストリームをトラックに分離し、オーディオトランスフォームを適用して、再び結合して使用することができます。

meetingV2/audiotransform/volume.tsという新しいファイルを作成します。このファイルに、ビデオトラックとオーディオトラックの両方を含むストリームにAudioNodeを適用するヘルパー関数を定義します。

function addAudioNodeToCombinedStream(context: AudioContext, node: AudioNode, inputStream: MediaStream): MediaStream {
  const audioTracks = inputStream.getAudioTracks();

  // This is a new stream containing just the audio tracks from the input.
  const audioInput = new MediaStream(audioTracks);

  // These are the input and output nodes in the audio graph.
  const source = context.createMediaStreamSource(audioInput);
  const destination = context.createMediaStreamDestination();

  source.connect(node);
  node.connect(destination);

  // Now create a new stream consisting of the gain-adjusted audio stream
  // and the video tracks from the original input.
  const combinedStream = new MediaStream(destination.stream);
  for (const v of inputStream.getVideoTracks()) {
    combinedStream.addTrack(v);
  }

  return combinedStream;
}
TypeScript

ヘルパーを使って、gain nodeにコンテンツ共有ストリームを適用します。

import { DefaultDeviceController } from 'amazon-chime-sdk-js';

export function addAudioVolumeControlToStream(inputStream: MediaStream): { stream: MediaStream, setVolume?: (volume: number) => void } {
  // Handle the case where this is a silent screen share: just
  // return the input stream with no volume adjustment.
  if (!inputStream.getAudioTracks().length) {
    return { stream: inputStream };
  }

  // This is the Web Audio context to use for our audio graph.
  const audioContext: AudioContext = DefaultDeviceController.getAudioContext();

  // This node applies a gain to its input. Start it at 1.0.
  const gainNode = audioContext.createGain();
  gainNode.gain.setValueAtTime(1.0, audioContext.currentTime);

  // This function lets you adjust the volume. It uses a quick linear ramp
  // to avoid jarring volume changes.
  const setVolume = (to: number, rampSec = 0.25): void => {
    gainNode.gain.linearRampToValueAtTime(to, audioContext.currentTime + rampSec);
  }

  // Now apply the node to the stream using the helper.
  const stream = addAudioNodeToCombinedStream(audioContext, gainNode, inputStream);
  return {
    setVolume,
    stream,
  };
}
TypeScript

meetingV2.tsにaddAudioVolumeControlToStreamをインポートします。

import { addAudioVolumeControlToStream } from './audiotransform/volume';
TypeScript

これで、meetingV2.tscontentShareStartの定義を拡張して、コンテンツ共有ストリームに音量調整機能を追加することができます。1つ目のswitchケースを置き換え、2つ目を修正し、ファイルの先頭のインポートブロックにContentShareMediaStreamBrokerのインポートを追加します。

import {
  …
  ContentShareMediaStreamBroker,
  …
} from 'amazon-chime-sdk-js';

…

    switch (this.contentShareType) {
      case ContentShareType.ScreenCapture: {
        const contentShareMediaStreamBroker = new ContentShareMediaStreamBroker(this.meetingLogger);
        const mediaStream = await contentShareMediaStreamBroker.acquireScreenCaptureDisplayInputStream();
        const { stream, setVolume } = addAudioVolumeControlToStream(mediaStream);
        (window as any).setVolume = setVolume;
        await this.audioVideo.startContentShare(stream);
        break;
      }
      case ContentShareType.VideoFile: {
        …
        const { stream, setVolume } = addAudioVolumeControlToStream(mediaStream);
        (window as any).setVolume = setVolume;
        await this.audioVideo.startContentShare(stream);
        break;
      }
    }
TypeScript

前述のようにリビルドとリローンチを行います。下の例にのように共有タブでオーディオ共有を行います。

A screenshot of Google Chrome's screen share dialog, showing a tab being selected and the share audio checkbox checked

先ほど変換したマイク入力と同様に、window.setVolumeを使って、共有タブで再生しているものの音量を調整できるようになりました。

これらの変更をテストする方法の詳細については、後述の「シングルマシンでのテスト」を参照してください。

スピーチ中のコンテンツ共有のダッキング

先ほど追加したコードで、コンテンツ共有のオーディオストリームの音量を調整できるようになりました。この調整のダッキングを実装するためには、ユーザーが話しているときにリアルタイムでトリガーする必要があります。

AudioVideoFacadeインターフェースは、リアルタイム・オブザーバー・インターフェースを公開しています。realtimeSubscribeToVolumeIndicatorを参加者のIDと一緒に使うことで、その参加者の入力音量をモニターすることができます。これは、デモ・アプリが誰が話しているかを示すために使用しているオブザーバーと同じです。

ユーザー自身のAttendeeIdを使用することで、マイクの入力を監視することなく、音声検出のためのきちんとしたインターフェースが得られます。同様の方法は、入力グラフにAnalyserNodeを追加することでも実装できます。ロビービューでは、このようにしてマイク入力のプレビューをアニメーションで表示しています。

ユーザーのマイクにVoiceFocusTransformDeviceを介してAmazon Voice Focusを使用する場合、Amazon Voice Focusはほとんどの非音声を含む環境ノイズを低減するように設計されているため、ボリュームは人間の音声のみを表すべきです。

setVolume 関数の呼び出しは、音量を下げるときには非常に短いランプタイムで、元の音量に戻すときには長いランプタイムで行うことで、応答時間とスムーズさのバランスを取っています。

次のメソッドでは、コンテンツ共有の開始イベントと停止イベントを利用して、動作の有効化と無効化を行います。

/**
 * Use the volume of the speaker to reduce the volume of content share.
 */
private configureDucking(setContentVolume: ((vol: number, rampSec?: number) => void)): void {
  const callback = async (
    _attendeeId: string,
    speakerVolume: number | null,
    _muted: boolean | null,
    _signalStrength: number | null
  ): Promise<void> => {
    if (speakerVolume > 0.1) {
      setContentVolume(0.1, 0.05);
    } else {
      setContentVolume(1.0, 0.5);
    }
  };
  
  const me = this.meetingSession.configuration.credentials.attendeeId;

  const observer: ContentShareObserver = {
    contentShareDidStart: () => {
      this.audioVideo.realtimeSubscribeToVolumeIndicator(me, callback);
    },

    contentShareDidStop: () => {
      this.audioVideo.realtimeUnsubscribeFromVolumeIndicator(me, callback);
      this.audioVideo.removeContentShareObserver(observer);
    },
  };

  this.audioVideo.addContentShareObserver(observer);
}
TypeScript

この新しいメソッドを使って、会議のデモでcontentShareStartを再実装することができます。

private async contentShareStart(videoUrl?: string): Promise<void> {
  const startAndDuck = async (mediaStream: MediaStream) => {
    const { stream, setVolume } = addAudioVolumeControlToStream(mediaStream);
    if (setVolume) {
      // This won't be set if this is a silent video stream.
      this.configureDucking(setVolume);
    }
    await this.audioVideo.startContentShare(stream);

    this.toggleButton('button-content-share', 'on');
    this.updateContentShareDropdown(true);
  };

  switch (this.contentShareType) {
    case ContentShareType.ScreenCapture: {
      const contentShareMediaStreamBroker = new ContentShareMediaStreamBroker(this.meetingLogger);
      const mediaStream = await contentShareMediaStreamBroker.acquireScreenCaptureDisplayInputStream();
      return startAndDuck(mediaStream);
    }

    case ContentShareType.VideoFile: {
      const videoFile = document.getElementById('content-share-video') as HTMLVideoElement;
      if (videoUrl) {
        videoFile.src = videoUrl;
      }
      return startAndDuck(await this.playToStream(videoFile));
    }
  }
}
TypeScript

このコードを導入すると、ユーザーが発言したときにコンテンツのシェアボリュームが自動的にダッキングされるようになります。このコードを使って録音したデモをご紹介します。

Credit: NASA’s Goddard Space Flight Center (the SDO Team, Genna Duberstein and Scott Wiessinger, Producers)

1台のマシンでのテスト

このシナリオを1台のコンピュータでテストすることは可能ですが次のような準備と2つのオーディオ出力デバイスが必要です。内蔵スピーカーとUSBまたはBluetoothヘッドセットがあれば十分です。

Google Chromeで2つのウィンドウを使用し異なるオーディオデバイスを選択できるようにします。1つはプレゼンター用、もう1つはリスナー用になります。またハウリングを防ぐために内蔵のオーディオ機器はミュートにします。

音声出力デバイスの設定

ヘッドセットをコンピュータに接続します。Macの場合ヘッドフォンジャックにヘッドフォンを接続するとOSが自動的にスピーカーを無効にするため、有線のヘッドフォンは使用できません。USBヘッドセット、Thunderboltドックなどの外部オーディオ機器、またはBluetoothヘッドフォンが使用できます。

デバイスを接続したら、Macの内蔵スピーカーがデフォルトのデバイスになっていることを確認します。

macOSではメニューバーの音量インジケーターをクリックし、「内蔵スピーカー」がチェックされていることを確認します。必要に応じて、デバイス名をクリックして切り替えてください。

A screenshot of the macOS sound device picker

Windowsでは、設定>システム>サウンドを開き、出力デバイスをドロップダウンメニューから選択してください。

A screenshot of the Windows sound device picker

Google Chromeの設定

2つのウィンドウを開き、隣り合って配置します。プレゼンターを左にリスナーを右に配置します。デモアプリをそれぞれのウィンドウに1回ずつ、計2回開きます。まだ会議には参加しないでください。

プレゼンターのウィンドウで、新しいタブを開いてコンテンツを共有します。今回はNASAのビデオを使用しましたが、リズミカルな音楽でも問題ありません。ビデオの再生を開始します。スピーカーから音声が聞こえることを確認してください。

ビルトインオーディオ出力をミュート

次にデフォルトのオーディオデバイスの音量を下げます。ほとんどの場合ミュートキーを押すか、ボリュームスライダをゼロまでドラッグします。これでハウリングを防ぐことができ、複数のデバイスから同時に聞くのではなく、リスナーのコンテンツ共有音声だけを聞くことができます。

ミーティングへの参加

リスナーのデモアプリのタブで、会議に参加し、上部のマイクをクリックして自分をミュートします。スピーカーアイコンの横にあるドロップダウンをクリックし、出力としてヘッドホンまたはヘッドフォンを選択します。

プレゼンターのデモアプリのタブで、同じ会議に参加しますが、音声出力はデフォルトのままで、ミュートされた内蔵スピーカーにします。

あなたが話すとプレゼンターのミュートされていないマイク入力があなたの声を拾い、リスナーのミュートされていないヘッドフォン出力があなたのヘッドフォンで自分の声を聞くことができます。

次にプレゼンターのウィンドウで、カメラボタンをクリックしてコンテンツを共有します。表示されたダイアログボックスでビデオとオーディオを再生しているタブを選択し、左下の「オーディオを共有」チェックボックスをクリックし、右下の「共有」をクリックします。

これで内蔵スピーカーからは何も聞こえなくなりますが、ヘッドフォンからは自分の声と、それまでミュートになっていたコンテンツ共有の音声が聞こえるようになります。話しを始めると、コンテンツ共有音声の音量が一時的に小さくなります。

クリーンアップ

デモ会議の終了は、どちらのデモウィンドウでも右上の赤いボタンを忘れずにクリックしてください。開いていたタブを閉じたり、ヘッドフォンを外したり、スピーカーのミュートを解除したりすることができます。

この記事に沿ってデモのサーバーレスアプリケーションをデプロイした場合、将来的なコストの発生を避けるために、CloudFormationスタックを削除することを忘れないでください。

結論

このブログ記事では、Web Audio APIを使ってマイク入力のオーディオトランスフォームを定義する方法と、そのトランスフォームを拡張してコンテンツ共有のオーディオに適用する方法を紹介しました。この2つをmeetingV2のデモに組み込み、さらにAmazon Chime SDKで利用可能なリアルタイムAPIを使用してスピーチの音量を監視することでダッキングを実装しました。最後に、2つのブラウザウィンドウで2つのオーディオデバイスを使用してテストしました。

これらの機能を自分のアプリケーションに拡張して、ボリュームコントロールを構築したり、他の種類のトランスフォーム(空間オーディオ用のパンナーノード、ディストーション、リバーブ、フィルタリング、コンプレッション、さらにはプログラムによるオーディオの生成など)を試してみましょう。MDNのWeb Audio APIドキュメントには、Web Audioができることについての概要が記載されています。

Amazon Chime SDKのドキュメントでは、音声変換デバイスやこれらのインターフェースと技術を使用してノイズ抑制を実装するAmazon Voice Focusについて詳しく知ることができます。ご質問やご提案がありましたら、コメント欄またはGitHub issuesからご連絡ください。

著者について

Richard Newman はAmazon Chime チームのPrincipal Engineerです。テレフォニーとWebブラウザの構築に精通しています。


参考リンク

AWS Media Services
AWS Media & Entertainment Blog (日本語)
AWS Media & Entertainment Blog (英語)

AWSのメディアチームの問い合わせ先: awsmedia@amazon.co.jp
※ 毎月のメルマガをはじめました。最新のニュースやイベント情報を発信していきます。購読希望は上記宛先にご連絡ください。

翻訳は BD山口とSA小林が担当しました。原文はこちらをご覧ください。