macOS の Firefox で MediaStreamAudioSourceNode 経由の mono MediaStream を PannerNode に通すと OS 出力でモノラル化される

macOS の Firefox で MediaStreamAudioSourceNode 経由の mono MediaStream を PannerNode に通すと OS 出力でモノラル化される

ブラウザ音声を PannerNode で空間音響化していたら、macOS の Firefox だけ OS 出力ではモノラルになる症状に当たりました。最小再現コードで段階的に切り分けたところ、mono の MediaStream を MediaStreamAudioSourceNode 経由で通したときだけ Firefox は OS 出力でステレオを失うと分かりました。
2026.05.15

はじめに

ブラウザ間で音声をやり取りする試作を作っているとき、受信側を macOS の Firefox にすると空間音響の左右パンニングが OS 出力に反映されない症状に当たりました。アプリ内に組んだ L/R モニタでは左右に動いています。

app monitor

OBS のキャプチャ波形と聴感ではモノラルになります。

obs monitor

調査したところ、macOS の Firefox では、mono の MediaStreamMediaStreamAudioSourceNode 経由で PannerNode に通すと、アプリ内 Web Audio チェーンでパンニングが計算されているにもかかわらず、OS のオーディオ出力でモノラル化されることが分かりました。入力ソースの MediaStream を最初からステレオで作っておけば、同じ MediaStreamAudioSourceNode → PannerNode の経路でも回避できます。

検証環境

  • macOS
  • Firefox 150.0.3 (64-bit)
  • Chrome 148.0.7778.168 (arm64)
  • OBS Studio でデスクトップ音声をキャプチャ
  • 内蔵スピーカで聴感を確認
  • 検証日 2026-05-15

対象読者

  • Web Audio API で空間音響を試したら、Firefox だけ左右パンニングが効かない症状に当たった人
  • WebRTC で受信した音声を PannerNode に通している人
  • ブラウザの問題だと決め込む前に、最小再現で切り分ける手順を確認したい人

参考

最小再現で切り分ける

アプリ内に L/R モニタを組み、同時に OBS でデスクトップ音声をキャプチャすることで、Web Audio チェーン内部と OS 出力の挙動を独立に観察できる状態にしました。

3 つの経路を比べます。

これらに加え、マイク mono 経路の後段で ChannelMerger(2) を挟んでステレオ化した経路 (マイク mono + ChannelMerger 展開) と、getUserMedia の音声処理を切った経路 (マイク mono + 音声処理オフ) も用意しました。各経路を Firefox と Chrome で動かし、パナー X 座標を -3+3 に振ったときの L/R モニタと OBS の挙動を比較した結果が次の表です。

経路 アプリ内 L/R モニタ OBS の波形と聴感
OscillatorNode 直結 左右に動く 左右に動く
マイク mono 経路 左右に動く モノラル
マイク mono + ChannelMerger 展開 左右に動く モノラル
マイク mono + 音声処理オフ ( echoCancellation: false, noiseSuppression: false, autoGainControl: false, channelCount: 2 ) 左右に動く モノラル
合成ステレオ経路 左右に動く 左右に動く

ここから分かることは次の通りです。

  • OscillatorNode 直結が両ブラウザで動いたので、PannerNode のパンニング計算自体は Firefox でも動いています。
  • マイク mono 経路で症状が再現しました。OscillatorNode 直結との差は「OscillatorNode 直」か「MediaStreamAudioSourceNode 経由」か、ここだけです。
  • マイク mono + ChannelMerger 展開 と マイク mono + 音声処理オフ で、後段のステレオ展開や getUserMedia の音声処理 (AEC / NS / AGC) のオフは効果がありませんでした。macOS 内蔵マイクは mono ハードウェアなので channelCount: 2 を要求しても sourceChannelCount は 1 のままになります。これらは getSettings() で実際に確認した値です。

つまり、後段で何をやっても OS 出力モノラル化が解けない、と言えます。

マイク mono 経路の主要部
experiments/firefox-panning-min/04-mediastream-mic.html
const micStream = await navigator.mediaDevices.getUserMedia({ audio: true });
const audioCtx = new AudioContext();
const { masterGain, analyserL, analyserR } = buildMasterChain(audioCtx);

const source = audioCtx.createMediaStreamSource(micStream);
const panner = audioCtx.createPanner();
panner.panningModel = 'equalpower';
panner.distanceModel = 'inverse';
source.connect(panner);
panner.connect(masterGain);
// パナー X 座標を動かすたびに panner.positionX 等を更新する

ステレオの MediaStream なら動く

合成ステレオ経路で経路を変えました。マイクは使わず、2 つの OscillatorNode (440Hz と 880Hz) を ChannelMerger(2) でステレオ化し、MediaStreamAudioDestinationNode に流すことで、入力ソースの段階からステレオな MediaStream を作ります。これを MediaStreamAudioSourceNode で受け直して PannerNode に通します。経路上の MediaStreamAudioSourceNode → PannerNode の部分はマイク mono 経路と同一で、入力 MediaStream のチャネル数だけが違います。

合成ステレオ経路の主要部
experiments/firefox-panning-min/08-stereo-mediastream.html
const oscL = audioCtx.createOscillator();
oscL.frequency.value = 440;
const oscR = audioCtx.createOscillator();
oscR.frequency.value = 880;

const merger = audioCtx.createChannelMerger(2);
oscL.connect(merger, 0, 0);
oscR.connect(merger, 0, 1);
oscL.start();
oscR.start();

const dest = audioCtx.createMediaStreamDestination();
merger.connect(dest);

// ChannelMerger で組み立てたステレオの MediaStream を、再度 MediaStreamAudioSourceNode で受け直す
const source = audioCtx.createMediaStreamSource(dest.stream);
const panner = audioCtx.createPanner();
panner.panningModel = 'equalpower';
panner.distanceModel = 'inverse';
source.connect(panner);
panner.connect(masterGain);

合成ステレオ経路を macOS の Firefox 150.0.3 で動かしたところ、アプリ内 L/R モニタも OBS の波形も左右に振り分けが出ました。つまり、同じ MediaStreamAudioSourceNode → PannerNode の経路でも、入力 MediaStream がステレオなら OS 出力でもパンニングが反映されます。

ここから原因が見えてきます。macOS の Firefox は、MediaStreamAudioSourceNode が mono の MediaStream を受け取った時点で「出力経路を mono に固定する」何らかの内部処理を持っており、後段の Web Audio チェーンでチャネル数を変えても無視している、と推測されます。PannerNode の W3C 仕様では出力は常にステレオと定義されているので、macOS の Firefox の挙動は仕様準拠の問題と言えます。

WebRTC で受け取った音声を空間音響化したい場合、標準の Opus codec はデフォルトで mono です。受信側が macOS の Firefox なら、まさにこの事象に直撃します。アプリ内 L/R モニタだけ見ているとパンニングは効いているように見えますが、OS 出力ではモノラルになります。

まとめ

macOS の Firefox では、mono の MediaStreamMediaStreamAudioSourceNode 経由で PannerNode に通すと、OS 出力でモノラル化される挙動を確認しました。入力ソースの MediaStream を最初からステレオで作っておけば、同じ経路でも回避できます。アプリ内モニタでパンニングが効いているように見えても OS 出力では別の挙動になっている可能性があるため、ブラウザ間の音声を扱う際は両方を独立に観察するのが安全です。

同様の問題に遭遇した方の参考になれば幸いです。なお、本記事の検証は macOS 上の Firefox 150.0.3 のみで実施しているため、他の OS や Firefox バージョンでは挙動が異なる可能性があります。

この記事をシェアする

関連記事