START を押したあと、1 小節目の前に 1 小節か 2 小節のカウントインを鳴らせるようにしました。曲の頭から合わせて弾き始めたいときに使えます。
これまでも、録音のときだけは 8 拍のカウントを入れられました。ただ、拍子に関係なく 8 拍で固定だったので、3/4 では 2 小節と 2 拍になり、1 小節目の頭がつかみにくいものでした。今回は、通常の再生でも録音でも、カウントインを同じ仕組みで鳴らし、長さを小節単位にしています。
この記事では、設定の分け方とカウントインの音、カウントインを音声のスレッドで鳴らす仕組み、タイマーや統計にカウントインを数えさせない方法を書きます。
通常再生時と録音時で分けて選ぶ
画面の下の帯にある SOUND を押すと、オーディオ設定が開きます。ここに、カウントインの行を 2 つ設けました。どちらも「なし・1 小節・2 小節」から選びます。

- カウントイン(通常再生時)。ふだんの再生で使う
- カウントイン(録音時)。録音待機で START を押したときに使う
録音待機(録音できるようにマイクを準備した状態)で START を押したときは録音時の設定を、それ以外は通常再生時の設定を使います。START を押すたびにどちらか片方の設定だけを使うので、2 つのカウントインが続けて鳴ることはありません。ふだんの練習ではカウントインなし、録音のときだけ 2 小節、のように使い分けられます。
1 小節の拍数は、今の拍子の拍数です。3/4 なら 1 小節で 3 拍、7/8 なら 7 拍鳴ります。テンポは 1 小節目と同じで、小節ごとに BPM を上げていくプログレッシブモードでは、開始の BPM です。タップテンポのように拍の途中から鳴らし始めたときは、カウントインを鳴らしません。
以前の録音の 8 カウントをオンにしていた人は、録音時のカウントインが 2 小節になっています。4/4 なら今までと同じ 8 拍で、3/4 なら 6 拍になります。
カウントインはクリックと別の音で鳴らす
カウントインは、どの音色を選んでいても、クリックとは別の高い電子音で鳴らします。サイン波を 80ms だけ鳴らす短い音で、小節の頭は 1760Hz、ほかの拍は 880Hz です。小節の頭を 1 オクターブ上げているので、2 小節のカウントインでも小節の区切りがわかります。
case "countIn": {
// 音量はマスターだけに従う(アクセント・ノーマルの音量を 0 にしていても聞こえる)
const gain = master * COUNT_IN_GAIN;
const stopAt = time + COUNT_IN_SECONDS;
voices.push({
source: {
type: "sine",
frequency: sound.accent ? 1760 : 880,
},
startTime: time,
endTime: stopAt,
gain: 1,
envelopes: [
[
{ time, value: 0, curve: "set" },
{ time: time + 0.002, value: gain, curve: "linear" },
{ time: stopAt, value: 0.001, curve: "exponential" },
],
],
fadesOutOnStop: false,
});
break;
}クリックの音色には電子音のピッ音もありますが、カウントインは音色の選択とは切り離しました。選んだ音色に寄せると、音色によっては 1 小節目からのクリックと区別がつかなくなるためです。音量はマスター音量にだけ従わせ、アクセントや、アクセントのない拍(ノーマル)の音量を下げていても聞こえるようにしています。ミュート中は鳴りません。
カウントインは音声スレッドのシーケンサーで鳴らす
このアプリの拍は、AudioWorklet の音声スレッドで動くシーケンサーが数えています。どの拍をいつどんな音で鳴らすかを決め、拍が鳴るたびにメインスレッドへ知らせます。
カウントインも、このシーケンサーに鳴らさせることにしました。再生を始めるときにカウントインの拍数を渡すと、シーケンサーはまずその拍数だけカウントインを進め、そのあとに 1 小節目を始めます。
advance: (untilTime) => {
while (!stopped && nextBeatTime < untilTime) {
if (countInRemaining > 0) {
advanceCountInBeat();
} else if (!advanceBeat()) {
break;
}
}
return takeOutput();
},カウントインの拍は、1 小節目からの拍の知らせ(beat)とは別の種類の知らせ(countIn)で送ります。録音は最初の beat で録り始め、小節タイマーは bar の知らせで小節を数えています。カウントインを beat として送ると、beat を受け取る処理のすべてに「カウントインなら数えない」という分岐が要ります。別の種類にしておけば、カウントインを扱わない処理は今までどおり動きます。
| {
type: "countIn";
time: number;
/** 小節の中の拍の番号 */
beat: number;
/** この拍を含めて残っているカウントインの拍の数 */
remaining: number;
interval: number;
pulses: BeatPulse[];
}逆に、カウントインのあいだも動いてほしい処理は、countIn も受け取るようにしました。拍の表示、振り子、拍に合わせた振動と画面のフラッシュです。画面の下の帯(START や経過時間が並ぶところ)では、カウントインのあいだだけ、小節数の代わりに残りの拍数を出します。

- カウントインのあいだも、拍の表示は拍に合わせて進む

- 経過時間は 0 のまま、小節数の代わりに残りの拍数(COUNT 3)が出る
録音の 8 カウントも、シーケンサーで鳴らすようにした
前の録音の 8 カウントは、シーケンサーの外で鳴らしていました。録音の機能がメインスレッドで AudioBufferSourceNode を 8 つ予約し、8 拍目のあとの時刻をシーケンサーに渡して、そこから 1 小節目を始めさせる形です。音は、BPM の検出の機能で使っているクリックの音でした。
この形だと、カウントインの拍はシーケンサーから見えません。画面の拍の表示や振り子は止まったままでした。また、8 拍を小節の長さに合わせるには、シーケンサーとは別に、メインスレッドの側でも拍子から拍数を計算する必要がありました。今回はシーケンサーの外で鳴らす 8 カウントをなくし、録音もシーケンサーのカウントインを使います。
録音の機能に残したのは、再生を始めるときに「録音待機中なら録音時の小節数を返す」関数だけです。コードの中では、メトロノームの再生の部分は録音の機能を直接呼ばない作りです。そこで、録音の機能がこの関数を登録しておき、再生の部分は再生を始めるたびにそれを呼びます。
const countInBeats =
startBeat === 0
? countInBeatsOf(
// 録音待機中は録音時の設定、それ以外は通常再生時の設定
resolveCountInOverride() ?? useUserSettingsStore.getState().countInBars,
state.timeSignature[0],
)
: 0;録音の帯には、カウントインのあいだ「COUNT IN」と出ます。この表示も、録音の機能が countIn の知らせを受け取って切り替えます。
タイマーと統計にカウントインを数えさせない
小節の数、小節タイマー、プログレッシブモードの 3 つは、カウントインを数えません。どれも、シーケンサーが数える 1 小節目からの小節をもとにしているためです。
直したのは、メインスレッドで時刻を測っている処理です。
- 秒・分のタイマー: 再生を始めた時点で
setIntervalを動かしていました。カウントインがあるときは、最初のbeatの知らせが届いてから始めます。 - 経過時間と統計: 再生を始めた時刻に、カウントインの長さ(拍数 × 60 ÷ BPM)を足した時刻を開始時刻にしました。統計はこの時刻から数えるので、カウントインの途中で止めた再生は記録されません。
経過時間と統計でこの方法を取れるのは、カウントインの長さが再生を始める前にわかるからです。1 小節目の頭が来るのを待たずに、開始時刻を先に決めておけます。
テスト
シーケンサーの単体テストでは、カウントインの拍の時刻と、小節の頭だけがアクセントになっていることを確かめます。1 小節目のアクセントが、カウントインの最後の拍のちょうど 1 拍あとに来ることも見ています。小節タイマーとプログレッシブがカウントインを数えないことも確かめています。
E2E(ブラウザでアプリを動かすテスト)では、鳴らした音の記録(音の種類と鳴る時刻)を集めて確かめます。カウントインの音は countIn という種類で記録されるので、4/4 の 1 小節なら 4 つ、3/4 の 2 小節なら 6 つあるかを数えます。録音では、カウントインのあとの 1 小節目から録り始めること、録音待機中は通常再生時のカウントインが鳴らないことを見ています。