Amazon Chime SDK for JavaScriptでオーディオやコンテンツを会議で共有する方法
バーチャル会議では参加者は通常の音声やビデオに加えて、リッチメディアを共有したいというお客様の要望を聞きます。Amazon Chime SDKは、アプリケーションやブラウザからのメディアを会議で共有することを可能にします。例えば、オンライン・フィットネス・クラスの音楽などです。オーディオやビデオはローカルコンピュータ、ローカルメディアファイル、ブラウザタブ、またはインターネット上の他のソースから共有できます。
しかし、音声やビデオのソースは必ずしも他の参加者と同条件で共有できる状態にあるとは限りません。音声の音量が大きすぎたり小さすぎたりして、リアルタイムに調整する必要があるかもしれません。また、ビデオやプレゼンテーションをトリミングしたり、回転させたりする必要があるかもしれません。
よくお客様から聞くのは講師やプレゼンターが話している間、あるいは翻訳者がライブで翻訳している間に、音楽やビデオのオーディオトラックの音量を一時的に下げることです。この作業をダッキングと呼びます。Amazon Chime SDK for JavaScriptでは、Webオーディオとビデオの変換パイプラインを利用して、これらの手順を表現するトランスフォームデバイスを定義することができます。
このブログ記事ではマイクの入力とコンテンツの共有音声に Webオーディオ トランスフォームを適用する方法を紹介します。さらに、このロジックをダッカーに拡張して、ユーザーが話している間にコンテンツ共有の音量を自動的に下げるために使用します。
前提条件
注:デモコードをデプロイして実行すると、AWSの料金が発生する場合があります。
ライセンス
このブログ記事に掲載されているコードは、amazon-chime-sdk-jsリポジトリの他のコードと同様に、Apache License 2.0の条件でライセンスされています。
はじめに
Amazon Chimeが他の参加者とオーディオを交換するために使用するマイク入力、オーディオエレメント出力、およびWebRTCレイヤーは、すべてウェブのmedia stream抽象化を利用して、アプリケーション内のオーディオの通過を定義されます。各ストリームは、オーディオまたはビデオの複数のトラックで構成されています。
Web Audioは入力ストリームを、audio context内のaudio nodeのグラフに接続するWeb技術です。これらのノードはオーディオトラックの変換や生成に使用できます。各ノードは少なくとも1つの入力または少なくとも1つの出力を持ち、多くの場合その両方を持ちます。オーディオノードについては、MDNのAudioNode
ドキュメントで詳しく説明されています。Amazon Chime SDKは、ビデオの処理についても同様のコンセプトを提供しています。詳しくは、Video Processorのドキュメントをご覧ください。
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 {private volume: number = 1.0; // So we can adjust volume prior to creating the node.async createSingleAudioNode(context: AudioContext): Promise { 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); }}}
TypeScriptmeetingV2.ts
に上述のクラスをインポートして、デモアプリケーションで利用できます。
import { VolumeTransformDevice } from './audiotransform/VolumeTransformDevice';
TypeScriptselectAudioInputDevice
の上にコードの一部を追加します。
// 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に入力スライダーを追加することができます。
HTMLwindow
オブジェクトのsetVolume
関数を格納する代わりに、入力の変更ハンドラにアタッチします。
document.getElementById('volume-in').onchange = setVolume;
TypeScriptアプリケーションにはVue、React、jQueryなどのフレームワークが使われていると思いますが、コンセプトは同じで、入力要素の変化を関連付けて、GainNode
でlinearRampToValueAtTime
を呼び出すまでの流れです。
コンテンツ共有の音量変更
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,};}
TypeScriptmeetingV2.tsにaddAudioVolumeControlToStreamをインポートします。
import { addAudioVolumeControlToStream } from './audiotransform/volume';
TypeScriptこれで、meetingV2.ts
のcontentShareStart
の定義を拡張して、コンテンツ共有ストリームに音量調整機能を追加することができます。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前述のようにリビルドとリローンチを行います。下の例にのように共有タブでオーディオ共有を行います。
先ほど変換したマイク入力と同様に、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 => { 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 {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このコードを導入すると、ユーザーが発言したときにコンテンツのシェアボリュームが自動的にダッキングされるようになります。このコードを使って録音したデモをご紹介します。