Unity WebGLでゲーム音声を録音する
現在開発してるシステムにて Unity WebGL で動かしているゲーム音声を録音する必要があったのでその備忘録を残します
問題点
Unity WebGL は内部で Web Audio API の AudioContext
を使用して音声を処理するが、これらの音声ノードは隠蔽されていてアクセスできない
コード
if (!window.isAudioContextPatched) {
window.unityAudioContext = null;
window.unityAudioNode = null;
const audioContextHandler = {
construct(target, args) {
if (!window.unityAudioContext) {
const context = new target(...args);
const audioNode = context.createGain();
audioNode.gain.value = 1.0;
const wrapConnectMethod = (createMethod, nodeType) => {
const originalCreate = context[createMethod];
context[createMethod] = function(...args) {
const node = originalCreate.apply(this, args);
const originalConnect = node.connect;
node.connect = function(destination, ...connectArgs) {
if (destination === context.destination) {
originalConnect.call(node, audioNode, ...connectArgs);
audioNode.connect(context.destination);
return audioNode;
}
return originalConnect.call(node, destination, ...connectArgs);
};
return node;
};
};
wrapConnectMethod('createBufferSource', 'BufferSource');
wrapConnectMethod('createGain', 'GainNode');
wrapConnectMethod('createOscillator', 'Oscillator');
// 適宜追加
const originalCreateMediaElementSource = context.createMediaElementSource;
context.createMediaElementSource = function(element) {element);
const source = originalCreateMediaElementSource.call(this, element);
source.connect(audioNode);
audioNode.connect(context.destination);
return source;
};
window.unityAudioContext = context;
window.unityAudioNode = audioNode;
}
return window.unityAudioContext;
}
};
window.AudioContext = new Proxy(window.AudioContext, audioContextHandler);
if (window.webkitAudioContext) {
window.webkitAudioContext = new Proxy(window.webkitAudioContext, audioContextHandler);
}
window.isAudioContextPatched = true;
JavaScript の Proxy
を利用して AudioContext
コンストラクタをラッピングすることで解決できる
Unity 音声の集約
const audioNode = context.createGain();
const wrapConnectMethod = (createMethod, nodeType) => {
const originalCreate = context[createMethod];
context[createMethod] = function(...args) {
const node = originalCreate.apply(this, args);
node.connect = function(destination, ...connectArgs) {
if (destination === context.destination) {
originalConnect.call(node, audioNode, ...connectArgs);
audioNode.connect(context.destination);
return audioNode;
}
return originalConnect.call(node, destination, ...connectArgs);
};
return node;
};
};
Unity が作成する音声ノードの connect
メソッドを置換し、作成した音声ノードを経由するようにしている
ラッピングの適用
wrapConnectMethod('createBufferSource', 'BufferSource');
wrapConnectMethod('createGain', 'GainNode');
wrapConnectMethod('createOscillator', 'Oscillator');
// 適宜追加
必要なメソッドをラップ
context.createMediaElementSource = function(element) {
console.log("Creating media element source:", element);
const source = originalCreateMediaElementSource.call(this, element);
source.connect(audioNode);
audioNode.connect(context.destination);
return source;
};
createMediaElementSource
は呼び出し時に connect
を呼ぶため別の対応が必要
Proxy による置換
window.AudioContext = new Proxy(window.AudioContext, audioContextHandler);
AudioContext(のコンストラクタ)を置換し、動作を変更
使用方法
public
にスクリプトを配置する
Next.js での実装例
import Script from 'next/script'
export default function UnityGamePage() {
...
return (
<>
<Script
src="/audioContextProxy.js"
strategy="afterInteractive"
/>
{/* Unityゲームのコンポーネント */}
<div id="unity-container">
{/* Unity WebGLビルドをここに配置 */}
</div>
{/* 録音制御UI */}
<button onClick={startRecording}>録音開始</button>
<button onClick={stopRecording}>録音停止</button>
</>
)
}
Unity のビルドファイル(.framework.js)で呼び出される window.AudioContext
を先に置換することで、Unity のコードを変更せずに動作を変えることができる
マイク音声の録音を行う必要がある場合は、元の AudioContext
/ AudioNode
を originalAudioContext
などに退避して使用する必要がある
あるいは Unity のビルドファイルを修正する方法もある
さいごに
Untiy 内部で録音(・録画)を行い、そのデータを JS に伝達する手法もあります(有料)
Recorder WebGL | Utilities Tools | Unity Asset Store
また Unity の AudioListner が受け取る音声を処理できる OnAudioFilterRead
は WebGL では使えませんでした
Discussion