拍子とグルーピング
拍子は、小節ごとに繰り返される強拍と弱拍のパターンです。拍子記号はそれを書き表したもので、分子は 1 小節に入る拍の数、分母は 1 拍にあたる音価を示します。4/4 なら 1 小節は 4 拍で、1 拍は 4 分音符です。
ビート追跡が返すのは、平坦な拍のリストです。そのリストを小節に変えるものが拍子です。1 小節が何拍を含み、どの拍から始まり、その内側のどこに副次的なアクセントが落ちるのかを与えます。
拍子はテンポではありません
テンポは拍がどれだけ速く来るかを表し、拍子は拍がどうまとめられるかを表します。同じ BPM の 2 曲が 4/4 と 7/8 でありうる — 拍の速さは同一で、小節の長さが違う、ということです。前者は テンポと BPM、後者が本ページです。
既存のビート列に対して拍子を採点する
estimateMeter は音声を受け取りません。受け取るのは 2 本の並行配列 — 拍ごとの時刻と、拍ごとのアクセント値 — で、それに対して候補の拍子を採点します。
import { estimateMeter } from '@libraz/libsonare-native';
const meter = estimateMeter({
beatTimes: analysis.beats.map((beat) => beat.time),
beatStrengths: analysis.beatObservations.onsetStrength,
candidateNumerators: [3, 4, 5, 6, 7],
});
console.log(meter.timeSignature.numerator, meter.timeSignature.denominator); // 7 4
console.log(meter.grouping); // [ 3, 2, 2 ]
console.log(meter.downbeatPhase); // 最初の小節が始まる拍のインデックス
console.log(meter.searched); // false は「何も採点されなかった」— 後述import libsonare as sonare
meter = sonare.estimate_meter(
[beat.time for beat in analysis.beats],
analysis.beat_observations.onset_strength,
candidate_numerators=[3, 4, 5, 6, 7],
)
print(meter.time_signature.numerator, meter.time_signature.denominator) # 7 4
print(meter.grouping) # [3, 2, 2]
print(meter.downbeat_phase)
print(meter.searched)sonare analyze song.wav --meter-candidates 3,4,5,7 --meter-denominator 4入力がビート列だけなので、すでに実行した解析を、パイプラインに触れずに採点し直せます。より広い候補集合に対して、あるいは 2 本の配列をスライスして、ビートの任意の区間 — 1 セクション、1 サビ — に対して、です。音声のデコードも、フレーム単位のオンセット包絡線の計算も発生しません。
C ABI のエントリポイントは sonare_estimate_meter_json(beat_times, beat_strengths, beat_count, options, out_json) です。オプションは構造体をゼロ埋めするのではなく sonare_meter_options_default() から作ってください。候補数がゼロの構造体は「既定値を使う」ではなく拒否として扱われます。
変拍子は、その分子を要求したときだけ報告されます
既定の候補集合は {3, 4, 6} です。5 や 7 や 11 はそこに含まれておらず、推定器は渡されていない分子を採点することが一切ないため、candidateNumerators を広げてその分子を入れるまで、変拍子には到達できません。7/8 の曲が 4 拍子として返ってくる理由として、これが最も多いものです。
候補集合を広げても、答えが広い拍子へ寄ることはありません。要求された分子はすべて同じ基準で比較され、3・4・5・6・7 から選ばせても、素直な 4 拍子はやはり 4 で返ります。リストは最大 16 要素で、各要素は [2, 32] の範囲です。空のリストは既定値へのフォールバックではなく、拒否されます。
結果は小節の長さだけでなく、小節がどう割れるかを示します
grouping は、小節内のアクセントのまとまりを、分子に合計される 2 と 3 のリストとして報告します。7 は素の 7 ではなく [3, 2, 2] — アクサク拍子が 3+2+2 と記譜するかたち — として返り、並びはアクセントに従うので、同じ分子でも [2, 3, 2] や [2, 2, 3] が別々の結果として返りえます。
分子だけでは区別できない拍子も分けられます。3+3 にアクセントのある 6 は [3, 3] を、2+2+2 にアクセントのある 6 は [2, 2, 2] を報告します。どちらも同じ分母の上の 6 であり、両者を見分けるのは grouping だけです。
要素が 1 つの grouping の読み方
grouping の要素が 1 つなら、割り方が解決されなかったということです。原因は 3 つあります。分子に 2 と 3 への分割が存在しない、分子がグルーピング探索の範囲(16)より広い、あるいは探索そのものが走らなかった、です。最後のケースを前の 2 つから分けるのが searched です。
計測値として使う前に読むべき 3 つのプロパティ
searched が他のすべてを左右します。 ビートが 8 拍未満 なら推定器は探索しません。候補は 1 つも採点されず、searched は false のまま、他のフィールドはすべて結果ではなく固定のフォールバックを載せます。分子は 4、分母は要求したもの、downbeatPhase は 0、grouping は未分割、candidateScores はすべて 0、candidates は 1 要素、timeSignature.confidence は 0 です。1〜7 拍は受け付けたうえでこの形で答えます。空の列は採点するものがないため "estimateMeter: beatTimes must not be empty" として拒否されます。この 0 が要点です。確認せずに読んだ場合は「中程度の検出」ではなく「不明」の側へ倒れますし、曲全体の解析も、採点できるビートが 8 拍に満たなければ同じ 0 を返します。それでも弱い測定値ではありません。 何も比較されていない値なので、測定値として表示しないでください。隣に並ぶ 4/4 は検出結果とまったく同じ見た目であり、フォールバックと結果を分けるのは searched です。まずそれを確認してください。
拍の単位は、要求したとおりに返ります。 1 拍が 3 分割されるかどうかは拍と拍のあいだのエネルギーから計測されるもので、拍ごとのアクセント列はそれを持っていません。したがってこの経路では複合拍子は解決されません。3+3 にアクセントのある 6 は、6/8 へ昇格されるのではなく、指定した分母のまま grouping を [3, 3] として返します。compoundSubdivisionThreshold もこの経路では一切効きません。音声を持つ曲全体の解析はこれを解決し、分母 8 を自ら報告します。
candidateScores は 1 つの結果の内側でしか比較できません。 スコアは採点されたビート数の平方根に比例して伸びます。繰り返すアクセントの根拠はそのように積み上がるからです。つまり同じ拍子でもビート数が 2 倍なら、スコアはおよそ 1.41 倍になります。長さの違う区間どうしを比べるセグメンテーション探索では、まず長さの影響を正規化で取り除く必要があります。あわせて注意したいのは、candidateScores は要求した分子と並行に並ぶのに対し、candidates は順位だという点です。そちらの k 番目は k 番目の要求ではなく、k 番目に有力な仮説です。
同じフィールド名を持つ 2 つの信頼度
timeSignature.confidence と candidates[k].confidence は、どちらも confidence という名前で、どちらも [0, 1] に収まりますが、測っているものが違います。
timeSignature.confidenceはマージンです。candidateScoresの 1 位と 2 位の差を読みます。スコアは標準化されているので、この差はノイズ何個分かという量で、それを0.45 + 差 / 6として[0, 1]に収めたものです。完全な同点なら 0.45、候補が 1 つだけで比較相手がなかった場合も 0.45 です。複合拍子か単純拍子か決着しなかった 6 は固定で 0.15 を引かれ、6/8 へ昇格した 3 は 0.55 以上に引き上げられます。candidates[k].confidenceは割合です。 各候補の正のスコアを、全候補の正のスコアの合計で割ったものです。要素の合計は 1 になり、支持の内訳として読めます。0.6 の候補は根拠の 60 % を持っているのであって、60 % の確率で正しいわけではありません。負のスコアは何も寄与しません。
そのため、明確な 3 拍子は candidates[0].confidence に支持のほぼ全部を載せながら timeSignature.confidence は 1 を大きく下回ることがあり、根拠が割れているクリップでも次点がたまたま大きく離れていれば高いマージンを報告します。両者は互いに比較できず、1 つのしきい値を共用することもできません。0.6 のマージンと 0.6 の割合は別の状況を表しています。意図したフィールドから値を読み、どちらを表示するにせよ、その名前で表示してください。
このデモの棒はマージンではなく割合です。4 が支持の大半を取り、6 が残りを拾います。4 拍子の 1 小節おきの強拍は、6 拍のまとまりの開始点でもあるからです。この残りは根拠の内訳をそのまま示したもので、1 位の割合を「1 位が正しい確率」と読んではいけない理由でもあります。
配列の並び順。 candidates は 2 つのキーで安定ソートされています。分子 と 分母の両方が timeSignature と一致する要素が先頭に固定され、残りは割合の降順で続きます。このエントリポイントでは先頭に固定された要素が最大の割合でもあります。オンセット包絡線がないので、選ばれる拍子は常に最高スコアの分子だからです。両者がずれるのは曲全体の解析だけです。そこでは timeSignatureCandidates が同じ規則で作られますが、音声があります。そのため、3+3 にアクセントのある 6 が複合分割の検定に落ちると、3 と 4 のうちスコアの高いほうに置き換えられ、その代替は、置き換えた 6 より小さい割合のまま先頭に固定されます。選ばれた拍子がどの要素とも一致しない場合 — 6 の候補が 6/4 として並んでいるところへ 3 が 6/8 に昇格した場合 — は timeSignature そのものがマージンの信頼度を持ったまま先頭に挿入され、後ろの割合は合計 1 ではなくなります。candidates[0] は「選ばれた拍子」、最大の割合は「最も強い根拠」として読んでください。ふつうは同じ要素ですが、常にではありません。
どのアクセント列を渡すか
AnalysisResult.beatObservations.onsetStrength を渡してください。これはライブラリ自身のダウンビート処理が採点している、拍の周囲の窓で集約されたオンセット強度です。
各 Beat が持つ strength は別物で、拍の位置で取得したオンセット包絡線の生の 1 フレームです。上限がなく、スケールは素材に依存し、拍位置のわずかなぶれでも動きます。表示に使うぶんには問題ありませんが、アクセントの採点には向きません。
渡せるのはこの 2 つです。第 3 の作り方 — 各拍の時刻で onsetEnvelope のフレームを 1 つ自分で読む — はどちらでもなく、2 つのどちらにもないサンプルレート依存を抱えます。包絡線のホップはサンプル数で数えられるため、1 フレームが覆う時間はレートごとに異なり、拍の時刻が同じでも拍で読める値はレートで変わります。同じ波形を 32000 Hz、44100 Hz、48000 Hz でサンプリングし、拍の時刻をサンプルと完全に一致させた場合でも、選ばれた分子はそれぞれ 6、3、4 でした。読み取りを拍の周囲の窓に広げても、この依存は消えません。ブラウザではこれが実害になります。ページは出力デバイスのレートでデコードするため、同じクリップが訪問者ごとに違う拍子を返します。beatObservations.onsetStrength は 3 つのレートすべてで同じ分子を選びます。
libsonare がどう計算するか
MeterAnalyzer は、候補となる各分子の拍位置へアクセント値を畳み込み、ダウンビート位置を区間全体に対する標準化された差として採点します。グループ平均から全体平均を引き、グループサイズの平方根を掛けて、区間自身のばらつきで割ったものです。この平方根の項があるおかげで、分子が広いというだけの理由で — ダウンビートに乗る拍が少ないというだけの理由で — スコアが下がることがなくなります。そして同時に、スコアが測定区間の長さに左右される理由でもあります。
グルーピングは、分子を 2 と 3 へ分割するすべての組み合わせとして列挙され、2 が先に出力されます。同点 — アクセントのない小節が生むのがこれです — のときに最も細かく割った読みが報告され、複合拍子の読みはビート側の根拠がそれを示したときにだけ採る、という設計です。各分割は副次アクセントの位置だけで採点され、subdivisionWeight で重み付けされたうえで、その分子のすべてのグルーピングが共有する位相スコアに加算されます。選ばれるのは標準化スコアが最も高い分子です。timeSignature.confidence は 0.45 の下限の上に、1 位と 2 位の差を kConfidenceMarginScale(6)で割って足したもの、candidates の各要素の信頼度はその分子の正のスコアを正の合計で割ったものです。探索を省く判定は beats.size() < 8 で、候補が採点される前に行われます。
estimateMeterFromBeats は同じ採点器を空のオンセット包絡線で呼んだものです。これが複合分割の分岐を無効にします。分母 8 への昇格には拍と拍の中点で取得したフレームが必要であり、拍ごとの列には取得すべき中点が存在しないからです。