Live2Dモデルを動画にレンダーする(WebCodecs API)

その他

Cubism SDK For Web ではCanvas要素上にLive2Dモデルを描画します。

また、WebCodecs APIを使うとCanvas要素の内容を動画に保存できます。

これらを組み合わせればLive2Dモデルの動画を作成できるのではないかと思い、試してみました。

※ 動作確認はWindowsのChromeで行いました

※記事内のソースコードは概略で、実際のコードではありません。

ブラウザ上での動画エンコード

WebCodecs API には VideoEncoder インターフェースがあり、これを使って動画エンコードを行います。

まず、VideoEncoder.isConfigSupported() メソッドを使ってエンコード設定がサポートされているか確認します。非同期関数であることに注意。

TypeScript
const videoEncoderSupport = await VideoEncoder.isConfigSupported({
    width: 1280, // フレーム幅
    height: 720, // フレーム高さ
    framerate: 30, // フレームレート
    latencyMode: 'quality', // レイテンシモード: "quality" | "realtime"
    bitrateMode: 'quantizer', // ビットレートモード: "constant" | "quantizer" | "variable"
    codec: 'hvc1.1.6.L123.00', // コーデック: "avc1.420034" | "hvc1.1.6.L123.00" | "vp09.00.10.08" | "av01.0.04M.08"
    hardwareAcceleration: 'prefer-hardware', // ハードウェアアクセラレーション: "prefer-hardware" | "prefer-software"
})
console.log("エンコード設定", videoEncoderSupport.supported, videoEncoderSupport.config)

VideoEncoderインスタンスを作成。
output時にmuxerへ動画チャンクを追加しますが、muxerについては後述。

TypeScript
const videoEncoder = new VideoEncoder({
    output(chunk, metadata) {
        muxer.addVideoChunk(chunk, metadata) // マルチプレクサに関しては後述
    },
    error(e) {
        console.error('VideoEncoder error', e)
    },
})

レンダーループ内でCanvas要素からVideoFrameを作成し、VideoEncoder.encode() メソッドでエンコードします。

TypeScript
async function encodeFrame(deltaTime: number) {
    // canvasからVideoFrameを作成
    const frame = new VideoFrame(canvas, {
        timestamp, // マイクロ秒
    })
    try {
        // フレームをエンコードキューに追加
        videoEncoder.encode(frame, encodeOption)
    } finally {
        frame.close()
    }
    // エンコード待ちのフレームをフラッシュ
    await videoEncoder.flush()
    // マイクロ秒に変換して加算
    timestamp += deltaTime * 1_000_000 
  })
}

音声のエンコード

音声のエンコードは AudioEncoder インターフェースを使います。大体VideoEncoderと同じような感じ。

AudioEncoder.isConfigSupported() メソッドでエンコード設定がサポートされているか確認します。
audioQueryという変数はVOICEVOX APIの音声合成クエリです。エンコーダーに入力する音声形式とエンコーダー設定は一致させておく必要があります。

TypeScript
const audioEncoderSupport = await AudioEncoder.isConfigSupported({
    codec: 'mp4a.40.2',     // コーデック:"aac" |  "opus" | "flac" | "mp3" など
    sampleRate: audioQuery.outputSamplingRate ?? 24_000, // サンプリングレート
    numberOfChannels: audioQuery.outputStereo ? 2 : 1,   // チャンネル数
})

AudioEncoderインスタンスを作成。muxerについては後述。

TypeScript
const audioEncoder = new AudioEncoder({
    output(chunk, metadata) {
        muxer.addAudioChunk(chunk, metadata) // マルチプレクサに関しては後述
    },
    error(e) {
        console.error('AudioEncoder error', e)
    },
})

音声データをエンコードします。wav変数は自作のWavFileReaderクラスです。
wavファイルのデータはbitsPerSampleが16bitなら符号付き整数(s16)、8ビットの場合は符号なし整数(u8)で格納されているらしいです。

TypeScript
const wav = // VOICEVOX APIで音声合成
const audioData = new AudioData({
    data: wav.getBody(),
    format: wav.getFormat().bitsPerSample === 16 ? 's16' : 'u8',
    numberOfChannels: wav.getFormat().channels,
    numberOfFrames: wav.getNumSamples(),
    sampleRate: wav.getFormat().sampleRate,
    timestamp: 0,
})
audioEncoder.encode(audioData)
await audioEncoder.flush()

今回、動画と音声を別々にエンコードしているので音声のタイミングは事前に計算しておく必要があります。

音声データの作成手順は以下のような感じ

1.動画と同じ長さの無音Wavデータを作成

2.あらかじめ計算しておいた位置をVOICEVOXの音声WAVデータで上書き

3.動画と同じ長さの音声データの完成。これをエンコーダに入力して動画とMUXします。

Wavはデータ構造が単純なのでこういう事ができて便利ですね。

動画と音声のMUX

EncodedVideoChunkやEncodedAudioChunkはそのままでは動画ファイルに保存できないため、コンテナに詰める必要があります。
今回は webm-muxer を使ってMatroskaコンテナに詰めました。

設定のcodecにはMatroskaのコーデックIDを指定します。MicrosoftのMatroska Media Container (MKV) のサポートページが参考になりました。

TypeScript
  // マルチプレクサを初期化
  const muxer = new Muxer({
    target: videoBuffer,
    type: 'matroska',
    video: {
      width: canvasWidth,
      height: canvasHeight,
      frameRate: framerate,
      codec: 'V_MPEGH/ISO/HEVC',
      alpha: true,
    },
    audio: {
      codec: 'A_AAC',
      sampleRate,
      numberOfChannels,
    },
    // ...

const videoEncoder = new VideoEncoder({
    output(chunk, metadata) {
        muxer.addVideoChunk(chunk, metadata) // ビデオチャンクをマルチプレクサに追加
    },
    error(e) {
        console.error('VideoEncoder error', e)
    },
})

const audioEncoder = new AudioEncoder({
    output(chunk, metadata) {
        muxer.addAudioChunk(chunk, metadata) // オーディオチャンクをマルチプレクサに追加
    },
    error(e) {
        console.error('AudioEncoder error', e)
    },
})

// ... エンコード処理 ...

// マルチプレクサの終了処理
muxer.finalize()
// マルチプレクサのバッファを取得
const buffer = muxer.target.buffer
// バッファからBlobを作成
const blob = new Blob([buffer], { type: `video/x-matroska; codecs="avc1.42C01E, mp4a.40.2"` })
// BlobからURLを作成
const videoUrl = URL.createObjectURL(blob)

デモ動画

Live2Dモデルを使用して、音声合成とリップシンクを行いながら、会話をレンダリングします。

おわりに

WebCodecs APIでのエンコードが意外と高速・高品質で、単純に動画エンコーダーとして使えそうだと思いました。

主要ブラウザもサポート(CanIUse)しているので、なにかに使えるかもしれませんね。

コメント

タイトルとURLをコピーしました