開発ブログ

BLOG

拍とポリリズムを、時計のように回るリングで表示する

針が 1 小節で 1 周するリングの拍の表示を作った。ポリリズムは 2 つのリングに分け、2 つの音が同時に鳴る位置を線で結ぶ。拍の印を円の上にどう置いたかと、針が音とずれないように、針の位置を音を鳴らす処理と同じ計算で決めた話を書く。

公開 2026年10月12日

表示設定の「拍の表示」で、「リング」を選べるようになりました。針が 1 小節で 1 周し、円周に並べた拍の印を通るたびに光ります。外のリングに拍、内のリングにパターンの音の位置を置き、ポリリズムでは 2 つのリズムを外と内のリングに分けて、同時に鳴る位置をリングのあいだの線でつなぎます。

表示設定のモーダル。番号 1 は「拍の表示」の「リング」のボタン、番号 2 は左の見本に出たリング
表示設定の「拍の表示」
  1. 拍のメーター・振り子に「リング」が加わった
  2. 見本も、選んだ表示で動く

Metronome.fm の「パターン」では、1 拍の中に鳴らすガイド音(細分・ポリリズム・カスタムビート)を選べます。この記事では、パターンの音をリングの印としてどう並べたかと、針が音とずれないように、針と音で拍の数え方をどう揃えたかを書きます。

外のリングに拍、内のリングに細分

1 周は 1 小節です。4 拍子なら、外のリングの真上・右・真下・左に拍の印を置き、針が真上から時計回りに回ります。

4 拍子・8 分の細分のリング。番号 1 は右の拍の印(光っている)、番号 2 は内のリングの裏拍の印、番号 3 は真下の短い印(ミュートの拍)
4 拍子で、パターンを 8th(8 分音符)にし、3 拍目をミュートにしたところ
  1. 2 拍目の印。針が通ると光る
  2. 内のリングの印。8 分の裏拍の位置
  3. ミュートの拍は短く薄く描き、光らせない

外の印は目盛りのような線で、長さを拍の強さ(アクセント・ノーマル・ミュート)で変えています。アクセントは長く、ミュートは短く描きます。針が通ると、アクセントの拍は画面の強調色(アクセントカラー)で、ノーマルの拍は文字の色で光り、ミュートの拍は光りません。

内のリングには、パターンで選んだガイド音の位置を置きます。音を鳴らす処理は、ガイド音を拍の頭からどれだけ後に鳴らすかを、拍の単位(0 以上 1 未満)で返す関数で決めています。リングもこの関数をそのまま使い、返った値に拍の番号を足して小節の拍数で割って、1 周の中の位置にします。

inner: accents.flatMap((_, index) =>
  getGuideNoteOffsets(guideNoteType, index, {
    polyrhythmRatio,
    swingRatio,
  }).map((offset) => (index + offset) / beatsPerBar),
),

カスタムビート(1 拍を 4 つに分けたマス目に音を置くパターン)も同じ形です。外の印は各拍の頭のマスの強さ、内の印は拍の中で鳴るマスの位置にします。どちらの印も、音を鳴らす処理がマス目を音の並びに直すのと同じ計算から求めているので、スウィングで裏拍を後ろにずらしたときも、ずらした位置がそのまま印に出ます。

ポリリズムは周期で 1 周させる

ポリリズム n 対 m は、m 拍のあいだに n 回、等しい間隔で音を鳴らします。リングでは 1 周を小節ではなく m 拍の周期にし、外のリングに拍の音(クリック)を m 個、内のリングに n 回鳴るほうの音を n 個置きます。中央には比を表示します。

ポリリズム 3:2 のリング。番号 1 は真上の、外と内のリングをつなぐ線、番号 2 は中央の「3:2」
ポリリズム 3:2。外に 2 拍、内に 3 つの音
  1. 2 つの音が同時に鳴る位置。リングのあいだを線でつなぐ
  2. ポリリズムの比
if (guideNoteType === "polyrhythm") {
  const [notes, beats] = polyrhythmRatio;
  return {
    cycleBeats: beats,
    outer: Array.from({ length: beats }, (_, index) => ({
      position: index / beats,
      level: 1,
    })),
    inner: Array.from({ length: notes }, (_, index) => index / notes),
    centerLabel: `${notes}:${beats}`,
    countsTotalBeats: true,
  };
}

外の印は、すべてノーマルの長さで描きます。拍の強さは小節の中の位置で決まるためです。たとえば 3/4 で 3 対 2 を鳴らすと、周期(2 拍)と小節(3 拍)の長さが違うので、同じ印に来る拍が、周によって小節の 1 拍目になったり 3 拍目になったりします。そのかわり、針が印を通ったときの光り方(強調色か文字の色か)は、そのとき鳴った拍の強さで決めています。

重なる位置は「i × m = j × n」

外の j 番目の印は j / m、内の i 番目の印は i / n の位置にあります。2 つが重なるのは i / n = j / m、つまり i × m = j × n のときです。3 対 2 なら周期の頭の 1 か所だけです。6 対 4 のように比が約分できるときは、4 拍のあいだに 3 対 2 を 2 回くり返す形になり、重なる位置は 2 か所に増えます(重なる数は n と m の最大公約数です)。

パターンの欄。番号 1 は「Poly 6:4」のボタン、番号 2 は「音の数 6」「拍の数 4」のステッパー
パターンの欄で、ポリリズムを 6 対 4 にしたところ
  1. ポリリズムのボタン。下に選んでいる比が出る
  2. 音の数と拍の数(2〜9)

印の位置は割り算で求めた小数なので、重なっているかどうかは、差がごく小さいかどうかで見ています。

const COINCIDE_EPSILON = 1e-6;

/** 内のリングの印のうち、外のリングの印と重なる位置(ポリリズムで 2 つの音が同時に鳴るところ) */
export const coincidentPositions = (outer: number[], inner: number[]) =>
  inner.filter((position) =>
    outer.some((other) => Math.abs(other - position) < COINCIDE_EPSILON),
  );

重なる位置には、内のリングから外のリングまでの線を引き、針が通るとアクセントカラーで光らせます。

針の位置は、音と同じ数え方で決める

針の位置は、拍の時計から読んだ「いまの拍」と「拍の中の進み具合(0〜1)」から計算します。拍の時計は、音声のスレッドから届く拍の知らせと音声の時計から、いまの拍の位置を求める部品です。1 小節で 1 周するなら、小節の中の拍番号で足ります。

ポリリズムでは足りません。3/4 で 3 対 2 を鳴らすと、周期(2 拍)が小節(3 拍)をまたぐからです。そのためポリリズムの音は、小節の中の拍番号ではなく、再生を始めてからの通しの拍番号で周期を数えて鳴らしています。2 小節目の 1 拍目は通しで 4 拍目なので、2 拍の周期では 2 拍目に当たります。小節の中の拍番号で針を置くと、この拍を周期の頭として扱ってしまい、音とずれます。

拍の時計も再生を始めてから数えた拍数を持っていますが、使えませんでした。カウントイン(鳴らし始める前の予備の拍)も数えているからです。そこで、音声のスレッドで動くシーケンサー(拍を刻んで音を鳴らす部分)が拍ごとに送る知らせに、通しの拍番号をそのまま入れるようにしました。

emit({
  type: "beat",
  time: beatTime,
  beat: currentBeat,
  bar: barCount,
  totalBeat: totalBeatCount,
  interval: beatDuration,
  pulses,
});

音を鳴らす側が数えているのと同じ値なので、マルチトラックでトラックが切り替わって数え直すときも、針と音で数え方が食い違いません。カウントインの拍には通しの拍番号がないので、そのあいだだけ小節の中の拍番号を代わりに使います。

const index = layout.countsTotalBeats
  ? (beat.totalBeat ?? beat.beat)
  : beat.beat;
const beatInCycle = ((index % cycleBeats) + cycleBeats) % cycleBeats;
return {
  position: (beatInCycle + beat.progress) / cycleBeats,
  count: beat.count,
  level: accents[beat.beat] ?? 1,
};

毎フレーム、SVG の属性だけを書き換える

針と印の光り方は、拍の表示の「振り子」と同じく、requestAnimationFrame で毎フレーム描きます。React の再描画はせず、ref で持った SVG の要素の transform と opacity だけを書き換えます。

印を光らせておく時間は、拍の長さの 0.3 倍です。32 分音符のように印と印の間隔が短いときは、隣の印と同時に光らないよう、印の間隔の 0.8 倍までに縮めます。

再生を止めたときや再生し直したときは、針の本来の向きがその場で変わります(止めたときは真上)。そのときは針を跳ばさず、回して戻します。その瞬間の向きと本来の向きとの差を覚えておき、毎フレームその差を少しずつ 0 に近づけます。

動きを減らす設定では、印から印へ跳ばす

OS で動きを減らす設定にしているときは、針を滑らかに回しません。針は直前に通った印の位置に置き、拍や内のリングの音が鳴るたびに次の印へ跳ばします。

/** 位置 position の直前(同じ位置を含む)にある印の位置 */
export const snapToMark = (position: number, marks: number[]) => {
  const passed = marks.filter((mark) => mark <= position + COINCIDE_EPSILON);
  return passed.length > 0 ? Math.max(...passed) : 0;
};

どの印がいつ光るかは滑らかに回るときと同じなので、針が印のあいだを動かないだけで、拍の位置は読み取れます。

全画面では、リングだけを大きく出す

全画面にすると、ふだんリングの下に出ている拍の番号の列を消し、リングと BPM だけを画面いっぱいに出します。リングの大きさは画面の縦横から決めていて、スマホを縦にしても横にしても、収まる大きさになります。演奏の動画を撮るときの背景にも使えます。

全画面のポリリズム 6:4 のリング。番号 1 は真上の、アクセントカラーで光った線、番号 2 は真下の線
全画面で、ポリリズム 6 対 4 を鳴らしているところ
  1. 周期の頭で重なる位置。針が通ったところで光っている
  2. 周期の真ん中で重なる位置

アプリの録音で録った動画に、リングを重ねて書き出すことは、まだできません。録音の機能は拍の時刻と小節の頭しか残していないので、ポリリズムの比やカスタムビートの並びも一緒に残すところから作る必要があるためです。

リングは表示を変えるだけで、鳴る音はパターンのとおりです。使い方はマニュアルの拍の表示(リング)にまとめています。

この記事を共有

X でポストはてなブックマーク