これまでのプログレッシブモードは、BPM の範囲(低い方と高い方)のうち、低い方から始めて、決めた間隔ごとに一定の量だけ上げる、という 1 通りの動きでした。間隔は設定の「インターバル」のことで、小節・秒・分のどれかで数えます。速いテンポから始めて落としていく練習(ritardando)や、テンポが階段ではなく坂のように変わる練習はできませんでした。
そこで、プログレッシブモードで BPM を下げていけるようにしました。あわせて、2 つの進め方も選べるようになっています。1 つは、間隔ごとにまとめて変えるのではなく、拍ごとになめらかに変える設定です。もう 1 つは、進んでは少し戻すのを繰り返す、波のような進め方です。
この記事では、3 つの設定をどういう形にしたかと、メトロノームの音を鳴らすシーケンサー(拍を刻んで音を鳴らす部分)の中でどう計算しているかを書きます。
下げる向き: 範囲の欄はそのままにして、「向き」を選ぶ形にした
「プログレッシブ設定」の先頭で「向き」を選べます。「下げる」を選ぶと、範囲の高い方から始めて低い方へ下げていきます。

- 向き。上げる(accelerando)か、下げる(ritardando)か
- 変わり方。段階的か、拍ごとになめらかに変えるか
設定の形は 2 通り考えました。1 つは、開始の BPM と目標の BPM を自由に置き、目標が低ければ下げる形です。もう 1 つは、範囲を「低い方・高い方」のまま持ち、上げるか下げるかを別の設定にする形です。
後者にしました。範囲を 2 本の針で指す BPM の定規、針どうしを 10 BPM 以上離す決まり、保存した設定のデータベースの項目、設定を書き出すファイルの形が、どれも「範囲の左が低い方」という前提で作られていたためです。前者にすると、その前提をすべて直すことになります。後者なら、変わるのは「どちらの端から始めるか」だけです。
コードでは、開始と終点の BPM を範囲と向きから求める関数を 1 つ作りました。これまで bpmRange[0] をそのまま開始の BPM として使っていたところは、この関数に置き換えています。
/** プログレッシブを始める BPM。up は範囲の低い方、down は高い方 */
export const getProgressiveStartBpm = (
range: readonly [number, number],
direction: ProgressiveDirection,
) => (direction === "down" ? range[1] : range[0]);画面の大きな数字は「開始 → 終点」の順に出すので、下げる向きでは「180 → 120」になります。1 間隔ごとに変わる量も、「−5 BPM」のようにマイナスで出ます。

- 開始 → 終点。下げる向きでは高い方が先に出る
- 今の BPM。なめらかな変わり方なので、5 刻みでない値も出る
スクリーンリーダーが読み上げる針の名前(開始・終了)も、向きに合わせて入れ替えています。見た目では左が低い方のままですが、下げる向きでは右の針が開始です。
なめらかな変わり方: 1 拍で変える量を、間隔に占める割合で決める
「変わり方」で「なめらか」を選ぶと、間隔ごとにまとめて変えていた量を、拍ごとに分けて少しずつ変えます。変わる速さは段階的なときと同じで、1 間隔のうちに変化量ぶん動きます。
拍ごとに動かす量は、「この拍が 1 間隔の何割にあたるか」で決めます。間隔を小節で数える設定なら、1 拍は 1 間隔の「1 小節の拍数 × 間隔の小節数」分の 1 です。秒や分で数える設定なら、前の拍からの経過時間を間隔の長さで割った値です。
const amount = isBars
? 1 /
(Math.max(settings.timeSignature[0], 1) *
Math.max(settings.progressiveBpmIntervalBars, 1))
: (beatTime - previous) / Math.max(getIntervalSeconds(settings), 1e-6);小節で数える場合、1 間隔ぶん進んで小節の頭に着いたときの BPM は、段階的なときと同じになります。4/4・1 小節ごとに +4 BPM なら、段階的では 60, 64 と飛ぶところが、なめらかでは 60, 61, 62, 63, 64 と拍ごとに上がります。
なめらかに変えると、BPM は 61.25 のような小数になります。拍の長さ(60 ÷ BPM 秒)は小数のまま計算し、画面に出す BPM のほうは四捨五入した値にしました。シーケンサーは音声のスレッドで動いていて、画面の状態とはメッセージで値をやり取りします。そのため、四捨五入した値が変わった拍でだけ画面へ知らせ、毎拍の通知は送らないようにしています。
終点に着いたあとの扱いは、段階的なときに合わせました。段階的では、終点の BPM も 1 間隔ぶん鳴らしてから、ループするか、マルチトラックの次のトラックへ進みます。なめらかでも、終点に着いてから 1 間隔ぶん鳴らすまで待ちます。ただし、なめらかでは小節の途中で終点に着くことがあるので、小節で数える設定では、1 間隔を鳴らし終えたあと、次の小節の頭まで待ってから次のトラックへ移ります。小節の途中でトラックが切り替わらないようにするためです。
波: 上げ幅だけ進んだら、戻し幅だけ戻す
「波(進んでは少し戻す)」をオンにすると、決めた幅(上げ幅)だけ上げたら、決めた幅(戻し幅)だけ一気に戻す、という動きを終点に着くまで繰り返します。少し速すぎるテンポで弾いてから楽なテンポに戻る、という練習のしかたです。

- 波のオン・オフ
- 上げ幅。下げる向きでは「下げ幅」になる
- 戻し幅
波の指定は、上げ幅と戻し幅の 2 つです。上げる途中は、ふだんの変化量と間隔のまま進みます。範囲 100〜140・BPM増加量 +5・上げ幅 20・戻し幅 15 なら、次のように進みます。
波 | BPM |
|---|---|
1 回目 | 100 → 105 → 110 → 115 → 120 |
2 回目 | 105 → 110 → 115 → 120 → 125 |
3 回目 | 110 → … → 130 |
4 回目 | 115 → … → 135 |
最後 | 120 → … → 140(終点) |
この動きを作るには、今の BPM だけでは足りません。たとえば 115 は、1 回目の波の途中でも 2 回目の波の途中でもありえます。上げ続けるのか、そろそろ戻すのかを決められません。そこで、シーケンサーは今の BPM と一緒に「次に戻し始める BPM」を覚えておくようにしました。コードでは、この BPM を波の「山」と呼んでいます。
// 波の山に届いていれば、進む代わりに戻し幅だけ戻す
if (course.wave && hasReached(state.bpm, state.waveTarget, sign)) {
return { state: fallBack(state, course), reachedEnd: false };
}
const target = course.wave ? state.waveTarget : end;
const bpm = notBeyond(state.bpm + sign * course.increment, target, sign);戻した先から上げ幅を足した値が、次の山になります。山が終点を越えるときは終点で止めます。下げる向きでは、足し引きの向き(コードの sign)を反対にするだけで、「下げ幅だけ下げたら戻し幅だけ上げる」になります。
戻し幅は、上げ幅より小さい値だけを選べるようにしました。戻し幅が上げ幅以上だと、1 回の波で少しも先へ進まず、終点に着かないためです。上げ幅のスライダーを動かして戻し幅以下にしたときは、戻し幅のほうが「上げ幅より 1 BPM 小さい値」に自動で変わります。
なめらかな変わり方と組み合わせると、上げるあいだは拍ごとに少しずつ上がり、戻すときだけ一気に戻ります。