メトロノームのクリック音を、左右どちらか片方だけで鳴らせるようにしました。イヤホンの片耳でクリックを聞き、もう片方で伴奏や自分の楽器を聞く、という使い方ができます。ライブで、イヤモニの片側だけにクリックを送るときにも使えます。
音を左右に振り分けることを、音響の言葉でパン(パンニング)と呼びます。Web Audio API のふつうのやり方でパンをかけると、このアプリの音では、片側に寄せた音が 2 倍の大きさになることがわかりました。この記事では、設定の画面と、パンをどこで計算しているか、音が大きくなりすぎないようにした方法を書きます。
設定の場所
画面の下の帯にある SOUND を押すと開くオーディオ設定に、「左右の出力」の欄を設けました。左・両方・右の 3 つから選びます。

- 左右の出力。押すと、すべての音の種類がその向きになる
少しずつ寄せられるスライダーではなく、3 択にしました。片耳に出す使い方では、中途半端に寄せる場面がないためです。
向きは、音の種類ごとに決められます。種類は、強く鳴らす拍のアクセント、それ以外の拍のノーマル、拍のあいだを細かく刻むガイド(ガイドノート)、クリック音に重ねる低い音のベース(ベース音)の 4 つです。詳細設定の「左右の出力(種類ごと)」で、たとえば拍のクリック音(アクセントとノーマル)だけを左に出し、ガイドは両方に残す、といった分け方ができます。

- 音の種類ごとの向き。この例では拍のクリック音(アクセントとノーマル)だけを左に出している
全体の 3 択は、4 つの種類をまとめて変えるボタンです。種類ごとに違う向きにしているときは、全体の 3 択はどれも選ばれていない状態になり、「音の種類ごとに分けています」と出ます。
全体の向きと種類ごとの向きを別々に覚えて組み合わせると、全体が左・ガイドが右のときに、ガイドがどちらから鳴るのかがわかりにくくなります。そのため、覚えておく向きは種類ごとの 4 つだけにしました。
StereoPannerNode を使わず、AudioWorklet の中でパンをかける
Web Audio API でパンをかけるなら、ふつうは StereoPannerNode をつなぎます。ただ、このアプリの音は AudioWorklet が作っていて、出力はもともと 2 チャンネル(左と右)です。Worklet は、鳴らす音(ボイス)を 1 つずつ出力のバッファに重ねて混ぜています。重ねるときに、左右それぞれの音量の倍率(係数)をかければ、ノードを増やさずにパンをかけられます。
種類ごとに向きを変えるには、StereoPannerNode だと種類の数だけ出力を分けてつなぐ必要があります。Worklet の中でパンをかければ、ボイスごとにパンの値を渡すだけで済み、メトロノームの音を MP3 のファイルに書き出す機能でも同じ計算を使えます。
片側に寄せると、音が 2 倍になる
StereoPannerNode には、もう 1 つ困るところがありました。入力がステレオだと、片側に寄せた音が大きくなります。
StereoPannerNode は、入力が 2 チャンネルのとき、寄せる側にもう片方の音をそのまま足します。左右に同じ音が入っている場合、左に寄せると左は 2 倍(+6dB)になります。ブラウザで、左右とも振幅 0.5 の音を通して確かめました。
入力 | 中央(パン 0) | 左だけ(パン -1) |
|---|---|---|
ステレオ(左右とも 0.5) | 左 0.5・右 0.5 | 左 1.0・右 0 |
モノラル(0.5) | 左 0.354・右 0.354 | 左 0.5・右 0 |
Worklet の出力は 2 チャンネルなので、Worklet の出力を StereoPannerNode につなぐと、表の上の行(ステレオ)になります。クリック音の音色のうち、生音の「メトロノーム」の音源は、左右にまったく同じ音が入ったステレオのファイルです。電子音の音色は 1 チャンネルで作り、左右に同じ音を出しています。どちらも、左に寄せるとクリック音が 2 倍の大きさで鳴ります。
そこで、寄せた側には左右を混ぜた音(左右の平均)を出し、反対側は消すことにしました。左右が同じ音なら、寄せた側の大きさは中央のときと変わりません。生音の「ドラム」の音色のように左右が少し違う音源も、平均を取るので大きくなりすぎません。
// パンを -1(左だけ)〜 1(右だけ)で受け取り、入力の左右から出力の左右への係数を返す
export const panMatrixOf = (pan: number): PanMatrix => {
const amount = Math.min(1, Math.abs(pan));
const near = 1 - amount / 2; // 寄せた側の、同じ側の音
const cross = amount / 2; // 寄せた側に混ぜる、反対側の音
const far = 1 - amount; // 反対側
return pan < 0
? { leftToLeft: near, rightToLeft: cross, leftToRight: 0, rightToRight: far }
: { leftToLeft: far, rightToLeft: 0, leftToRight: cross, rightToRight: near };
};パンが -1(左だけ)のとき、左には左右の音を半分ずつ混ぜて出し、右には何も出しません。パンが 0(両方)のときは、今までと同じく左は左、右は右です。
テスト
Playwright の E2E では、出力の音そのものを取り出せません。そのため、このアプリでは Worklet が音を鳴らした時刻を記録し、拍のずれを確かめる仕組みをもともと使っています。この記録にパンの値の項目を加え、どの音がどちらから鳴ったかを確かめています。
「左」を選んで再生すると、すべてのクリック音の記録がパン -1 になること、詳細設定でアクセントだけを右にすると、アクセントだけがパン 1 になることを見ています。両方(パン 0)の音には、記録にパンの項目を付けないので、下のノーマルの行は undefined になります。
const sounds = await playUntilClickCount(page, audioRecorder, 5);
expect(sounds.map((sound) => ({ kind: sound.kind, pan: sound.pan }))).toEqual([
{ kind: "accent", pan: 1 },
{ kind: "normal", pan: undefined },
{ kind: "normal", pan: undefined },
{ kind: "normal", pan: undefined },
{ kind: "accent", pan: 1 },
]);左右の係数(panMatrixOf が返す値)は、単体テストで確かめています。