MediaStreamTrackProcessorでAudioを扱う
はじめに
ブラウザのリアルタイム通信の仕組みとしてWebRTCがありますが、より細かい制御を行うことができるInsertable StreamやWebCodecsといった仕様が提案されれています。今回はその関連仕様であるMediaStreamTrackProcessorを使って、オーディオを扱う例を取りあげます。
MediaStreamTrackProcessorとは
MediaStreamTrackProcessorはMediaStreamから取り出したVideo/AudioのMediaStreamTrackを扱うための新しい仕組みです。2021年2月現在、まだW3Cでも非公式なドラフトとして提案されている状態です。
- Unofficial Draft: MediaStreamTrack Insertable Media Processing using Streams ... 2021.10.20現在リンク切れ
- Unofficial Draft: MediaStreamTrack Insertable Media Processing using Streams ... 2021.10.20現在こちら
- Unofficial Draft: MediaStreamTrack Insertable Media Processing using Streams ... 2022.02.16現在こちら
MediaStreamTrackProcessorを使うと、VideoのMediaStreamTrackからはViderFrameを、AudioのMediaStreamTrackからはAudioFrameをストリーム(ReadableStream)経由で取り出すことができます。
ところで2021年2月現在、MediaStreamTrackProcessorはデフォルトでは有効になっていません。Chrome 88以降で、次のフラグを有効(Enabled)にする必要があります。
- chrome://flags/#enable-experimental-web-platform-features
MediaStreamTrackProcessorでAudioを扱う方法
準備: MediaStreamTrackProcessorの生成
MediaStreamからAudioのMediaStreamTrackを取得し、それを引数にMediaStreamTrackProcessorのインスタンスを生成します。
const [audioTrack] = mediastream.getAudioTracks();
const processor = new MediaStreamTrackProcessor(audioTrack);
WebAudioで再生する場合
まずWritableStreamを作成し、そのwrite()ハンドラでAudioFrameを取得します。
AudioFrameからAudioBufferを取得し、それをWebAudioのAudioBufferSourceNodeを使って再生します。
MediaStreamTrackProcessorを作成したWritableStreamに接続することで、Audioデータを流し込むことができます。
// --- WebAudio AudioContextの準備 ---
const audioCtx = new AudioContext();
let audioTime = audioCtx.currentTime;
// --- WritableStreamを準備 ---
const writable = new WritableStream({
// --- AudioFrameが渡された時のイベント ---
write(audioFrame) {
// --- WebAudioを使って再生する ---
const source = audioCtx.createBufferSource();
source.buffer = audioFrame.buffer;
source.connect(audioCtx.destination);
source.start(audioTime);
audioTime = audioTime + audioFrame.buffer.duration;
},
// --- その他のイベント ---
start() {
console.log('Audio Writable start');
},
close() {
console.log('Audio Writable close');
},
abort(reason) {
console.log('Audio Writable abort:', reason);
},
});
// --- WritableStream を MediaStreamTrackProcessor のストリームに接続する ---
processor.readable.pipeTo(writable);
MediaStreamTrackGeneratorを使う場合
MediaStreamTrackProcessorを使うとMediaStreamTrackからAudioFrameを取り出すことができますが、MediaStreamTrackGeneratorを使うことで反対にAudioFrameからMediaStreamTrackを作り出すことができます。
// --- Generatorを用意 ---
const generator = new MediaStreamTrackGenerator('audio');
// --- MediaStreamTrackProcessor のストリームと接続する
processor.readable.pipeTo(generator.writable);
// --- MediaStreamに追加し、AudioElementで再生する --
const generatedStream = new MediaStream();
generatedStream.addTrack(generator);
audioElement.srcObject = generatedStream;
await audioElement.play();
そのまま戻しても意味がないため、通常は途中にTransformStreamを挟んでオーディオデータを加工します。
// --- TransformStreamを準備 ---
const transformer = new TransformStream({
// --- 変換処理 ---
async transform(audioFrame, controller) {
// --- AudioFrameからオーディオサンプルを取得 (Float32Array) ---
const samples = audioFrame.buffer.getChannelData(0); // 1チャンネルと仮定
// --- 加工を行う ---
for(let i = 0; i < samples.length; i++) {
samples[i] = samples[i] + (Math.random() * 2 - 1)*0.1; // ノイズを加える例
}
// --- 音オーディオサンプルをAudioFrameに戻す --
audioFrame.buffer.copyToChannel(samples, 0); // 1チャンネルと仮定
// --- 次のストリームに渡す ---
controller.enqueue(audioFrame);
}
});
// MediaStreamTrackProcessor のストリームと
// TransformStream、MediaStreamTrackGeneratorを接続する
processor.readable
.pipeThrough(transformer)
.pipeTo(generator.writable);
// --- この後再生する ---
WebCodecsでエンコード/デコードする場合
MediaStreamTrackProcessorで取り出したAudioFrameは、WebCodecsのAudioEncoderを使ってエンコード(圧縮)することが可能です。これを何かの方法(たとえばWebSocketやWebTrasport)でリモートに送り、AudioDecoderでデコード(復元)すれば音声として再生するここができます。
エンコードまで
まずWritableStreamを作成し、そのwrite()ハンドラでAudioFrameを取得し、AudioEncoderに渡します。AudioEncoderではエンコード後のデータを受けとるoutput()ハンドラが呼ばれるので、そこでリモート送信など次の処理を行います。
// --- Encoderを準備 ---
const audioEncoder = new AudioEncoder({
output: function (chunk) {
// --- エンコード後のデータを受け取る。ここでリモートに送ったりする ---
// ... 省略 ...
},
error: function () {
console.error(arguments)
}
});
// --- 設定 ---
const audioCtx = new AudioContext();
const audioSampleRate = audioCtx.sampleRate;
await audioEncoder.configure({
codec: 'opus',
sampleRate: audioSampleRate,
bitrate: '128000',
numberOfChannels: 1,
});
// --- WritableStreamを準備 ---
const writable = new WritableStream({
// --- AudioFrameが渡された時のイベント ---
write(audioFrame) {
// --- エンコードする ---
audioEncoder.encode(audioFrame);
audioFrame.close();
},
// --- その他のイベント ---
start() {
console.log('Audio Writable start');
},
close() {
console.log('Audio Writable close');
},
abort(reason) {
console.log('Audio Writable abort:', reason);
},
});
// --- WritableStream を MediaStreamTrackProcessor のストリームに接続する ---
processor.readable.pipeTo(writable);
デコード側
デコード側では、エンコードデータを受け取り、それをAudioDecoderでデコード(復元)後に再生します。たとえばWebAudioを使って再生する場合は次のようになるでしょう。
// --- AudioContextを準備 ---
const audioCtx = new AudioContext();
const audioSampleRate = audioCtx.sampleRate;
let audioTime = audioCtx.currentTime;
// --- Decoderを準備 ---
const audioDecoder = new AudioDecoder({
output: async function (audioFrame) {
// --- WebAudioを使って再生する ---
const source = audioCtx.createBufferSource();
source.buffer = audioFrame.buffer;
source.connect(audioCtx.destination);
source.start(audioTime);
audioTime = audioTime + audioFrame.buffer.duration;
},
error: function () {
console.error(arguments)
}
});
// --- Decoder設定 ---
await audioDecoder.configure({
codec: 'opus',
numberOfChannels: 1,
sampleRate: audioSampleRate,
bitrate: '128000',
});
// --- エンコード済みのオーディオデータを受け取る関数 ---
function handleEncodedChunk(chunk) {
// encoderから来たchunk
audioDecoder.decode(chunk); // 上記の output が呼ばれる
}
用意した関数handleEncodedChunk()をEncoderのoutput()ハンドラで直接呼び出せば、エンコード→デコードの処理を直結できます。あまり意味は無いですが、処理の理解の助けにはなるでしょう。
まとめ
まだ非公式ドラフト状態の MediaStreamTrackProcessor / MediaStreamTrackGenerator について取り上げました。対象としては、まだまだ情報が少ないオーディオ(Audio)を選び、WebAudioを使った再生の例を示しています。
また関連してWebCodecsの一部である AudioEncoder / AudioDecoder についても記載しました。将来的にはWebTransportとも組み合わせることで、自由(と苦労)が増えそうです。
Discussion