楽曲解析
楽曲解析は、録音を聴いてその中身を報告する libsonare の機能群です。テンポ、ビートの位置、キーとコード、セクション構成、メロディの動きを取り出します。
このページは目的別のガイドです。どの関数を呼ぶか、返ってきた値が実際には何を意味するか、そしてより詳しい説明がどこにあるかを示します。概念そのものは、すべての特徴量の土台であるスペクトログラムから始まる用語集が、フィールド単位のリファレンスは JavaScript 解析 API が担当します。どれを使うか決めるときにこのページを参照してください。
音声や MIR に初めて触れる場合は、先にオーディオ基礎(サンプル、サンプルレート、デシベル)と MIR の全体像(各特徴量の系統が何を測るか)を読んでください。
解析はパイプラインのどこに位置するか
解析は録音の中身を明らかにします。編集は 1 トラックを変え、ミキシングは複数をまとめ、マスタリングは仕上がったステレオを整えます。解析は他の工程が参照する段階でもあります。ジャンル別のマスタリング目標、検出ビートから引いたグリッド、ピッチ追跡の前に取り出したステムなどです。
このページで学べること
読み終えると、次のことができるようになります。
- テンポ、キー、ビート、コード、セクション、メロディ、音色、ダイナミクスを 1 回の呼び出しでまとめて得る
- 一括結果が粗すぎるとき、または必要なオプションが隠れているときに、個別のヘルパーを選ぶ
- 拍子推定の 2 つの
confidenceを取り違えず、実測とフォールバックを見分ける - キーの信頼度を、精度ではなくモデル自身の確信度として解釈する
- 3 つの音源分離ルートを使い分け、それぞれが何を取り出せて何を取り出せないかを把握する
- 互換レイヤーのほうが入口として適している場面を判断する
まず 1 回の呼び出しから
analyze(...) は全チェーンを実行し、すべてを一度に返します。ほとんどのアプリケーションではこれが最初の呼び出しとして正解です。メル、オンセット、ビート、コード、セクション、音色が共有する STFT を一度だけ計算し、そこから各推定器が答えを読み取ります。キーだけは、より長い窓(4096)のクロマを倍音・打撃成分の分離(HPSS)と音量重み付けつきで自前で作ります。密度の高い素材でキー精度が目に見えて上がるためです。
import { init, analyze } from '@libraz/libsonare';
await init();
// デコードはブラウザ、解析は libsonare。
const ctx = new AudioContext();
const response = await fetch('track.mp3');
const buffer = await ctx.decodeAudioData(await response.arrayBuffer());
const samples = buffer.getChannelData(0); // 左チャンネルだけで、モノラルミックスではない
const result = analyze({ samples, sampleRate: buffer.sampleRate });
console.log(result.bpm, result.bpmConfidence);
console.log(result.key.name, result.key.confidence);
console.log(result.timeSignature.numerator, result.timeSignature.denominator);
console.log(result.beats.length, result.downbeatIndices.length);
console.log(result.chords.length, result.sections.length, result.form);
console.log(result.chords[0]?.name, result.chords[0]?.romanNumeral);import { Audio, analyze } from '@libraz/libsonare-native';
const audio = Audio.fromFile('track.mp3');
const result = analyze({ samples: audio.getData(), sampleRate: audio.getSampleRate() });
console.log(result.bpm, result.key.name, result.timeSignature.numerator);
console.log(result.beats.length, result.chords.length, result.sections.length, result.form);from libsonare import Audio, analyze
audio = Audio.from_file("track.mp3")
result = analyze(audio.data, audio.sample_rate)
print(result.bpm, result.key.name, result.time_signature.numerator)
print(len(result.beat_times), len(result.chords), len(result.sections), result.form)sonare analyze track.mp3 --json
# 5 拍子も採点する(既定の候補は 3、4、6)
sonare analyze track.mp3 --meter-candidates 3,4,5,6getChannelData(0) が渡すのはステレオファイルの最初のチャンネルだけです。ざっと確かめるには足りますが、右に定位した音は抜け落ちます。ミックス全体を解析するには、先にチャンネルをサンプルごとに平均してください((left[i] + right[i]) / 2)。
この呼び出しは同期で、WASM ビルドはシングルスレッドです。短いクリップでなければ Web Worker で実行してください。analyzeWithProgress(...) は同じリクエストを受け取り、(progress, stage) を通知し、長い処理をキャンセルできます。解析オプションは analyze と同じく samples と並べても options の下に渡しても構いません。解析オプションにないキーは、そのキーを挙げた TypeError になります。
const result = analyzeWithProgress({
samples,
sampleRate,
options: { tuning: 'auto' },
onProgress: (progress, stage) => console.log(stage, progress),
cancel: () => userPressedStop,
});sampleRate は必ず渡す
解析系の関数は、sampleRate を省略すると 22050 Hz とみなし、食い違いを報告しません。44.1 kHz の録音をレートなしで解析すると半分の速さで読まれ、120 BPM のクリック列が 60 と返ります。テンポ、ビートやセクションの時刻、ピッチの周波数など、時間に関わる結果がすべて同じ倍率でずれます。ブラウザなら buffer.sampleRate のように、バッファ本来のレートをすべての呼び出しに渡してください。ルーム音響の関数は既定が 48000 です。
analyze() はトライアドしか探索しない
一括パスは useTriadsOnly を true にします。単体の detectChords(...) は false のままです。useTriadsOnly: false を渡すまで、セブンスコードはその中のトライアドとして返ります。拡張クオリティが必要なら明示的に渡してください。
すべての推定器が乗る共通チェーン
ここにあるものは、ファイルを読み直す別々のアルゴリズムではありません。デコード済みサンプルがフレームごとの短時間スペクトルになり、その 1 つの行列が特徴量の層へ分岐し、各推定器は必要な特徴量から判断を読み取ります。キーだけは例外で、サンプルから直接、より長い窓のクロマを組み立てます。
- STFT を経由して計算
- サンプルから直接読む
- 推定器:特徴量から 1 つの判断
この配置はコストの違いも説明します。analyze(...) は STFT を一度だけ計算し、同じ行列をキー以外のすべての推定器へ渡します(キーは自前でクロマを作ります)。単一の答えを返すヘルパーは毎回サンプルからチェーンを組み直すので、4 つ呼べばおよそ 4 回分の解析になります。ヘルパーは 1 つのフィールドだけが欲しいとき、または一括呼び出しが隠しているオプションが必要なときに使うもので、処理を節約するためのものではありません。
呼び出しの選び方
Node はブラウザと同じ名前です。Python は同じ名前のスネークケース(detect_bpm、analyze_sections、chord_functional_analysis)で、CLI の列には対応する sonare サブコマンドを示します。
| 知りたいこと | 呼び出し | analyze(...) に含まれるか | CLI | 解説の場所 |
|---|---|---|---|---|
| すべてを一度に | analyze(...) · analyzeWithProgress(...) | n/a | analyze | このページと JavaScript 解析 API |
| 速さ | detectBpm(...) · analyzeBpm(...) | 含まれる | bpm | テンポと BPM |
| ビートと小節線の位置 | detectBeats(...) · detectDownbeats(...) | 含まれる | beats · downbeats | ビートとダウンビート |
| すべての打点 | detectOnsets(...) · onsetEnvelope(...) | 含まれない | onsets | オンセット検出 |
| 拍子記号 | estimateMeter(...) | 含まれる | analyze --meter-candidates | 拍子とグルーピング |
| キー | detectKey(...) · detectKeyCandidates(...) | 含まれる | key | キー検出 |
| コード進行 | detectChords(...) · chordFunctionalAnalysis(...) | 含まれる | chords | コード認識 |
| 曲の構成 | analyzeSections(...) · detectBoundaries(...) | セクションのみ | sections | セクションと構成 |
| メロディの動き | analyzeMelody(...) · pitchYin(...) · pitchPyin(...) | メロディのみ | pitch | メロディとピッチ |
| 音色 | analyzeTimbre(...) · mfcc(...) · melSpectrogram(...) | 音色のみ | mel | メル・MFCC・音色 |
| 時間軸上の和声行列 | chroma(...) · chromaCqt(...) · nnlsChroma(...) | 含まれない | chroma | クロマ特徴量 |
| 生の時間周波数表現 | stft(...) · melSpectrogram(...) | 含まれない | n/a | スペクトログラムと STFT |
| モノラル/マルチチャンネルのミックスの分割 | hpss(...) · decomposeStems(...) · decomposeStemsLinked(...) | 含まれない | n/a | 後述の音源分離 |
ヘルパーを使うのは、より細かく制御したいとき(テンポ範囲を狭める、別のキープロファイルを使う、拡張クオリティを含めてコードを探索する)か、答えが 1 つだけ必要で残りが無駄になるときです。analyze(...) ではコードの HMM・キー文脈・転回形のスイッチが少し長い名前になっていて、useChordHmm・useChordKeyContext・detectChordInversions が、detectChords(...) の useHmm・useKeyContext・detectInversions に当たります。
リズム
統合時のつまずきはリズム周りに集中します。「テンポは何か」への妥当な答えが複数あり、confidence という名前のフィールドが 2 つあるからです。順に見ていきます。
ビート、ダウンビート、その根拠
ビート追跡は手拍子を打つような一定の拍を見つけ、ダウンビート検出はそのうちどの拍が小節の頭かを選びます。analyze(...) は両方と、判断に使った拍ごとの根拠を返します。
const { beats, beatTimes, downbeatIndices, beatObservations } = analyze({
samples,
sampleRate,
});
// beats[downbeatIndices[k]] が k 番目の小節頭。
const isDownbeat = new Set(downbeatIndices);
for (const [index, beat] of beats.entries()) {
console.log(beat.time.toFixed(3), isDownbeat.has(index) ? 'bar' : 'beat');
}| フィールド | 内容 |
|---|---|
beatTimes | 秒単位のビート位置を Float32Array で。 |
beats | 同じ位置をオブジェクトで表したもの。各要素が strength を持つ。 |
downbeatIndices | 検出した小節頭ごとに 1 つの、beats へのインデックス。並行配列でも第 2 の時系列でもないので、ダウンビート判定は集合に含まれるかの判定で行う。 |
downbeatPhase | 曲頭が最初の小節の何拍目から始まるか。拍子推定器が出す値で、後からダウンビートが精緻化されても再計算されないため、downbeatIndices[0] と食い違うことがある。 |
beatObservations | 判断の根拠となるアクセント情報。onsetStrength、lowFrequencyEnergy、chordChange の 3 本で、いずれも 1 拍につき 1 値。解析が生成できなかったストリームは空で返り、これは「全拍が 0 点だった」とは別の状態。 |
beatLocalBpm | 各拍における平滑化テンポ。computeTempoCurve: true を指定しない限り空で、テンポが動く素材では adaptiveTempo: true も必要。 |
beats[].strength と beatObservations.onsetStrength は交換できません。前者はオンセット包絡線を拍の位置で 1 フレームだけ読んだ生の値で、上限がなく、拍位置の揺れにそのまま影響されます。後者は拍の周囲の窓で集約した値で、ライブラリ自身のダウンビート判定が採点しているのはこちらです。拍どうしのアクセントを比較する用途では onsetStrength を使ってください。
テンポ
| 経路 | 返るもの | 使いどころ |
|---|---|---|
analyze(...) の result.bpm | 数値 1 つと bpmConfidence、さらに primary / half / double / other のラベル付き bpmCandidates | すでに一括解析を回している場合。候補の関係ラベルは倍取り・半分取りを検知する手がかりになる。bpmConfidence は、勝ったテンポ系列が集めた周期性の票の割合(テンポと BPM)。 |
detectBpm(...) | 数値 1 つ | テンポだけが欲しい場合。 |
analyzeBpm(...) | bpm、confidence、candidates(選ばれたテンポが先頭、残りは得票率順)、その根拠である autocorrelation と tempogram | 根拠を見せたい、または範囲を絞って探索したい場合。既定値は bpmMin 30、bpmMax 300、startBpm 120、maxCandidates 5。 |
analyzeRhythm(...) | 局所的な拍周期から精緻化した独自の bpm と、グルーヴ、シンコペーション、規則性、beatIntervals | 数値よりもリズムの性格が欲しい場合。既定範囲は狭く bpmMin 60、bpmMax 200。 |
analyzeRhythm(...) の bpm は意図的に第 3 の数値です。ビート追跡が落ち着いたグリッドから測っており、合成クリック列に対する計測では、どちらのテンポ入口よりも既知テンポに近い値になります。4 つの入口が小数点まで一致することは期待しないでください。
拍子:estimateMeter
estimateMeter(...) は、渡したビート列の上で候補となる拍子記号を採点します。音声もフレーム単位の包絡線も不要なので、既存の解析結果の任意の区間(たとえば 1 セクション分)を、解析をやり直さずに採点できます。
import { analyze, estimateMeter } from '@libraz/libsonare';
const result = analyze({ samples, sampleRate });
const meter = estimateMeter({
beatTimes: result.beatTimes,
beatStrengths: result.beatObservations.onsetStrength,
candidateNumerators: [3, 4, 6], // 既定値
denominator: 4, // 既定値
});
console.log(meter.timeSignature, meter.grouping, meter.downbeatPhase);渡すのは beats[].strength ではなく beatObservations.onsetStrength です。どちらも事前スケーリングは不要で、採点前に系列自身の最大値で割られるため、読まれるのは内部のアクセント差だけです。onsetEnvelope(...) のフレームから自分で列を組み立てることも避けてください。その読み取り値はサンプルレートで変わり、ブラウザは出力デバイスのレートでデコードするため、同じクリップが訪問者ごとに違う答えを返します。実測は拍子とグルーピングにあります。
このデモでは断定よりも順位を見てください。素直な 4 拍子を 4 小節鳴らすと、支持の大半は 4 が取りますが 6 にも相応の分が残ります。1 小節おきの強拍は 6 拍の区切りの開始点でもあるからです。
答えのもう半分は grouping で、小節が 2 拍と 3 拍のアクセント群にどう分かれるかを表します。7 拍なら [3, 2, 2] と返り、合計は必ず分子に一致します。要素が 1 つだけなら内部分割は解決されていません。3 拍ずつにアクセントが乗った 6 は、拍の内部分割がアクセントからは分からないため、要求された分母のまま [3, 3] として報告されます。
confidence という名前のフィールドが 2 つある
ここが最も間違えやすい箇所です。取り違えると、もっともらしく見えて意味の違う数値が表示されます。
| フィールド | 正体 | 値域と挙動 |
|---|---|---|
timeSignature.confidence | マージン由来の値。勝った候補が 2 位をどれだけ引き離したかを、スコア自身のノイズ単位で測って信頼度に写したもの。 | マージンが無い状態が 0.45 で、そこから上がります。つまり 2 候補が拮抗していても約 0.45 になり、確率ではなく分離度を表します。複合拍子か単純拍子か決着しない場合は一定量が引かれます。 |
candidates[k].confidence | 正規化された割合。各候補の正のスコアを全候補の合計で割った値。 | 支持の内訳として読め、合計は 1 になります。0.6 の候補は支持の 60 % を占めるという意味で、正解である確率が 60 % という意味ではありません。 |
したがって、明確な 3 拍子では candidates[0].confidence がほぼすべての支持を示していても timeSignature.confidence は 1 よりかなり低いままになりえますし、本当に曖昧なクリップでも 2 位がたまたま大きく離れていれば timeSignature.confidence は高く出ます。答えている問いが別なので、自分の問いに合うほうを選び、UI ではそれに合わせて表示し、両方を 1 つのしきい値に通すことは避けてください。それぞれの計算式は拍子とグルーピングにあります。
candidates は順位表(k 番目の要素が k 番目に良い仮説で、選ばれた拍子記号が先頭)で、candidateScores は要求した分子と並行に、渡した順で並びます。両者のインデックスは対応しません。スコアは採点したビート数が多いほど大きくなるため、比較は 1 つの結果の中だけにしてください。どちらも拍子とグルーピングに詳しい説明があります。
短い区間ではそもそも探索されない
検出ビートが 8 個未満だと、推定器は探索を行いません。それでもフィールドの埋まった結果は返ります。4/4、timeSignature.confidence は 0、分割されていない grouping、候補 1 件、スコアは全部 0 です。そしてそれを教えてくれるのは searched だけです。信頼度が 0 なので、確認せずに読んだ場合は「中程度の検出」ではなく「不明」の側へ倒れます。それでも隣に並ぶ 4/4 は、検出結果とまったく同じ見た目です。
const meter = estimateMeter({
beatTimes: result.beatTimes,
beatStrengths: result.beatObservations.onsetStrength,
});
if (!meter.searched) {
// 何も計測されていない。timeSignature.confidence は 0 で、
// 他のフィールドはすべて固定のフォールバック値。
console.log('too few beats to estimate a meter');
} else {
console.log(meter.timeSignature.numerator + '/' + meter.timeSignature.denominator);
}このフォールバックはもっともらしい 4/4 です。短いクリップ、静かなイントロ、材料の乏しいビート追跡のいずれも同じ答えに行き着き、実測と区別できるのは searched だけです。描画する前に必ず確認してください。
無音の入力
デジタル無音にはテンポもキーも構成もないため、analyze(...) は推測ではなく「結果なし」の値を返します。リズムの規則性と安定度は 0、セクションは信頼度 0 の Unknown が 1 つ、isCompressed は false、音色の密度は 0 です。キーの信頼度は 24 候補に均等な事後確率(1/24)になります。
ハーモニー
キー
import { detectKey, detectKeyCandidates } from '@libraz/libsonare';
const key = detectKey({ samples, sampleRate, profile: 'temperley' });
console.log(key.name, key.shortName, key.confidence);
const ranked = detectKeyCandidates({ samples, sampleRate, modes: 'all' });
for (const { key: candidate, correlation } of ranked.slice(0, 3)) {
console.log(candidate.shortName, correlation.toFixed(3));
}Key.confidence は、採点されたすべての候補のプロファイル相関に対するソフトマックスです(analyze(...) の内部では、選ばれたキーのクロマに対する事後確率)。2 位が迫るほど下がるので、根拠が割れる 2 つのキー(典型的には平行調どうし)はそれぞれ約半分を報告します。つまり曖昧さの指標としては優秀ですが、精度の指標としては使えません。クロマがこのキーを候補集合からどれだけ決定的に選び出したかを示すだけで、その選択が実際に当たる頻度は示しません。注釈付き録音に対する学習も行っていないため、自信のある誤答は十分ありえます。この値で処理を分岐させる場合は、自分の素材で確かめてしきい値を決めてください。
detectKeyCandidates(...) は代わりに候補ごとの生の correlation を正規化せずに返します。単一の確信度ではなく判断の形そのものを調べたいときはこちらです。
key.mode は名前('major'、'minor'、または 'dorian' のような教会旋法)で返り、detectKey(...) はクロマの構築に使った tuning も返します。クロマは既定で A440 を基準に読まれます。半音の間にある録音には tuning: 'auto' を渡してずれを音声から測定し、使われた値を結果から読み取ります。同じオプションは detectChords(...) と analyze(...) にもあります。
const detected = detectKey({ samples, sampleRate, tuning: 'auto' });
console.log(detected.mode, detected.tuning); // 例: "major"、[-0.5, 0.5) のセミトーン端数| オプション | 効果 |
|---|---|
profile | 相関を取るキープロファイル。'ks' / 'krumhansl'(Krumhansl-Kessler)、'temperley'、'shaath'、3 種類の EDM 用、'bellman-budge' から選べます。名前で指定したプロファイルは固定され、genreHint より優先されます。省略すると、ジャンルヒントかクロマが選びます。ジャンルによる差が大きいので、結果が系統的にずれているときに最初に試す値です。 |
modes | 'major-minor'、'modal'、'all'、または明示リスト。モード集合を広げるとソフトマックスが正規化する候補プールも広がるため、首位が変わらなくても信頼度は下がります。 |
genreHint | 素材のジャンルは分かるがプロファイルまでは決められないときに、ヒューリスティックに選ばせる指定。profile を省略したときだけ働きます。 |
useHpss · loudnessWeighted · highPassHz | 相関を取る前にクロマを整えます。打撃成分を落とす、知覚的な音量で重み付けする、低域を削る、の 3 つ。 |
コード
import { detectChords, chordFunctions } from '@libraz/libsonare';
const chordOptions = {
useTriadsOnly: false, // 拡張クオリティを含める
useHmm: true, // フレームごとではなく系列として平滑化する
useKeyContext: true,
keyRoot: key.root,
keyMode: key.mode, // 名前でも Mode の序数でもよい
chromaMethod: 'nnls',
} as const;
const { chords } = detectChords({ samples, sampleRate, ...chordOptions });
for (const chord of chords) {
console.log(chord.name, chord.start.toFixed(2), chord.duration.toFixed(2), chord.confidence);
}
// 手元のコードにキーの中での機能を付ける。再検出はしない
const labelled = chordFunctions({ chords, key });
console.log(labelled[0].roman, labelled[0].function); // 例: "I", "tonic"chordFunctions(...) は同じ時刻付きエントリに、ローマ数字(roman)と和声機能('tonic'、'subdominant'、'dominant'、キーのスケール外のルートは 'chromatic'、N.C. は 'none')を加えて返します。chordFunctionalAnalysis(...) は、キーが分かっている場合に検出とラベル付けを 1 回の呼び出しで行い、同じエントリを返します。keyRoot は必須で、chordFunctionalAnalysis({ samples, sampleRate, keyRoot: key.root, keyMode: key.mode }) のように渡します。結果をプロジェクトにコード注釈として書き戻す方法は、検出したコードからプロジェクトの注釈へを参照してください。
各 Chord は root、bass、quality、start、end、duration、confidence、そしてコアが正規の綴りで与える name を持ちます。rootName、bassName、name はどのバインディングから読んでも同一です。analyze(...) 内のコードには result.key を基準にした romanNumeral も付き、N.C. では空になります。
クオリティの網羅範囲はトライアドや素のセブンスをはるかに超えています。maj、m、dim、aug、7、maj7、m7、m7b5、dim7、sus2、sus4、sus2add4、add9、madd9、maj9、9 に加えて、6、m6、mM7、7sus4、11、13、7b9、7#9 も認識します。オンコードはベース音を付け足して C/E のように表記されます。相関が threshold を下回った区間は、無理に推測せず不明として報告されます。useHmm を使った平滑化後の経路は、平滑化しない探索と同じく素のトライアドを優先します。また、コードの根拠でキーが動くのは、クロマ側の首位の事後確率の 4 分の 1 以上を持つキーに限られます。
| オプション | 効果 |
|---|---|
useTriadsOnly | ここでは false、analyze(...) の内部では true。上記のクオリティが欲しければ false のままに。 |
useHmm · hmmBeamWidth | 進行をフレーム単位ではなく系列としてデコードします。単フレームのちらつきはほぼ消えます。 |
useKeyContext · keyRoot · keyMode | 検出済みのキーに属するコードへ探索を寄せます。 |
chromaMethod | 'stft' か 'nnls'。後者は各音の倍音をその音自身に割り当てるので、強いベースの 3 倍音・5 倍音が G や E を光らせることがなく、密度の高い素材で有効です。 |
detectInversions | ベース音をルートに畳み込まず、別に報告します。 |
tuning | チューニングのずれを半音の分数単位でコード用クロマへ反映します。範囲は [-0.5, 0.5)、既定値は 0(A440)で、'auto' なら音声から測定し、結果は使われた値を報告します。analyze(...) でも同じオプションを使うと、キー・コード・セクションに使うクロマへ反映されます。 |
nFft | クロマのウィンドウ長。22050 Hz 換算のサンプル数で、入力のレートに合わせて再スケールされるため、どのレートでも同じ時間幅になります。 |
useBeatSync · minDuration · smoothingWindow | フレーム単位の判断を区間へまとめる方法を制御します。 |
構成とメロディ
セクション
import { analyzeSections, detectBoundaries } from '@libraz/libsonare';
const sections = analyzeSections({ samples, sampleRate, minSectionSec: 4 });
for (const section of sections) {
console.log(section.name, section.start.toFixed(1), section.energyLevel, section.confidence);
}
const { boundaries, noveltyCurve, noveltyPeak } = detectBoundaries({ samples, sampleRate });analyzeSections(...) はラベル付きの区間を返します。イントロ、A メロ(Verse)、サビ(Chorus)、ブリッジ、間奏(Instrumental)、アウトロ、または不明(Unknown)です。detectBoundaries(...) はその下にあるラベルなしの層で、遷移点と、それを拾い出した連続的なノベルティ曲線を返します。自前のしきい値を当てるにはこちらが必要です。セクション一覧を粗くした表現ではありませんし、どちらか一方から他方を導くこともできません。
想定しておくべき挙動が 2 つあります。プリコーラスには検出分岐が無いため、このラベルで絞り込むと常に空になります。そして不明が返る経路は 2 つで、どちらも confidence は 0 です。境界が見つからなかった(曲全体が 1 区間)場合と、静かでもなく他と反復もしない端の区間の場合です。見分け方はセクションと構成にあります。
noveltyCurve は自身の最大値でスケールされています。ピークの 1 は「この曲の中で最も変化の大きいフレーム」という意味であって「大きな変化」という意味ではありません。絶対しきい値と比較される生の応答に戻すには noveltyPeak を掛けます。
各ピークは、前後 referenceWindow 秒(既定 60)の範囲で最大のノベルティ値の threshold 倍に達する必要があります。そのため長い曲でも、支配的な変化が遠く離れた弱いセクション変化を隠しません。範囲内のどのイベントも 30% に届かない孤立した大きなバースト(短い大音量)は、その基準から除外されるので、周囲のセクション変化を隠しません。各セクションの energyLevel はフレーム RMS の中央値を最も大きいセクションで正規化した値なので、短いバーストが自分のセクションを最大にすることはありません。また、1 つだけ大きなセクションがあっても、ごく普通の最初や最後のセクションがイントロやアウトロと読まれることはありません。
analyzeSections(...) が受け取るのは nFft、hopLength、minSectionSec だけで、referenceWindow は既定の 60 秒のままです。それ以外の境界オプションは detectBoundaries(...) のものです。
| オプション | 既定値 | 意味 |
|---|---|---|
threshold | 0.3 | ピークが達する必要のある、局所基準に対する割合。 |
absoluteThreshold | 0.005 | 生のノベルティに対する下限。0 にすると threshold だけで判定するので、定常的な入力でも区切られる。 |
referenceWindow | 60 | 局所基準を取る片側の秒数。0 で相対しきい値を無効にでき、曲の長さ以上なら基準が曲全体になる。 |
peakDistance | 2 | ピーク間の最小間隔(秒)。強いピークから順に採用する。 |
kernelSize · nMfcc · nChroma · useMfcc · useChroma | 64 · 13 · 12 · true · true | チェッカーボード型カーネル(hopLength 単位のフレーム数。ホップ 512、22050 Hz で 64 フレームは約 1.5 秒)と特徴量の組み合わせ。useMfcc と useChroma のどちらかは有効のままにする。 |
メロディとピッチ
import { analyzeMelody } from '@libraz/libsonare';
const melody = analyzeMelody({ samples, sampleRate, usePyin: true, fmin: 65, fmax: 2093 });
console.log(melody.meanFrequency, melody.pitchRangeOctaves, melody.vibratoRate);
const voiced = melody.points.filter((point) => point.frequency > 0);ピッチ追跡は STFT ではなく波形を直接読み、モノフォニックの素材、つまり同時に 1 音だけが鳴る素材を前提とします。ソロボーカル、リードライン、あるいは先に分離したステムに対して使ってください。フルミックスに掛けると、その瞬間に支配的な音をフレームごとに追いかけます。
points[].frequency は無声フレームで 0 になるので、平均を取る前に除外します。usePyin: true は Viterbi で平滑化した追跡器(各フレームの候補に点数を付け、全体で最もなめらかな 1 本の経路を選びます。メロディとピッチ参照)を選びます。コストは上がりますが、素の YIN に比べてオクターブ飛びが格段に減るため、ユーザーに見せる用途では通常こちらです。既定の探索範囲は fmin 65 Hz から fmax 2093 Hz、YIN の threshold は 0.1 です(差分関数のくぼみがどこまで深ければ周期とみなすかを決める値で、小さいほど厳しくなります)。pYIN の有声判定はこの値ではなく voicedFlag で決まります。実際に扱う楽器に合わせて範囲を狭めるのが、最も手軽に精度を上げる方法です。
音源分離
倍音成分・打撃成分の分離、NMF による成分分解、共通マスクを使うマルチチャンネル処理は、音源分離で説明しています。音の比較デモと、各方式の再構成・位相に関する制約も確認できます。
その下の層
上のすべての推定器は特徴量を読んでいるだけで、その特徴量は直接使えます。推定器がカバーしていないものを自分で作るときに役立ちます。
import { stft, melSpectrogram, chroma, mfcc, onsetEnvelope, spectralCentroid } from '@libraz/libsonare';
const spectrum = stft({ samples, sampleRate, nFft: 2048, hopLength: 512 });
const mel = melSpectrogram({ samples, sampleRate, nMels: 128 });
const pitchClasses = chroma({ samples, sampleRate });
const timbre = mfcc({ samples, sampleRate, nMfcc: 13 });
const envelope = onsetEnvelope({ samples, sampleRate });
const brightness = spectralCentroid({ samples, sampleRate });nFft は 1 つの解析窓の長さ、hopLength は次の窓を何サンプル先から始めるかで、周波数の細かさと時間の細かさのトレードオフを決めます。結果を最も左右する 2 つのパラメータです。これらの特徴量関数は nFft を、渡したバッファのサンプル数として読みます。一方、音声を元のレートのまま受け取る解析器(ビート、テンポ、オンセット、キー、リズム、コード、音色)は nFft を 22050 Hz 換算のサンプル数として読み、入力のレートに合わせて再スケールします。そのため同じ nFft が 44100 Hz や 48000 Hz でも同じ時間幅になります。hopLength は入力のサンプル数のままです。解析窓は、22050 Hz 未満を含むどの入力レートでも、また音楽・セクション・境界の各解析器、音色のラフネス、ノート採譜のピッチ追跡でも、時間の量として扱われます。特徴量群はほかに CQT とその派生、トネッツ、スペクトルコントラスト、ロールオフ、平坦度、帯域幅、フラックス、フレームごとのレベルとゼロ交差率、そしてスペクトログラムを音声へ戻す逆変換も備えています。一覧は JavaScript 特徴抽出 API にあります。
このページか、互換レイヤーか
libsonare は同じ DSP を 2 通りに公開しています。このページの楽曲解析 API は音楽的な問いに沿った形をしていて、音楽的なオブジェクトを返します。互換レイヤーは Python の特徴抽出ライブラリ librosa に合わせた形で、関数名も引数名も配列の形も揃えてあります。
既存の Python 解析コードを移植する、公開されたパイプラインを再現する、手元にある結果と数値を突き合わせる、といった場合は互換レイヤーを選びます。新しいものを作るならこのページの API です。解釈が必要なクロマ行列ではなく、コード一覧をそのまま返してくれます。両者はコアを共有しているので、1 つのアプリケーションで混ぜても問題ありません。対応表と、意図的に異なる箇所については librosa 互換性 を参照してください。
関連ページ
- ビートとダウンビート · テンポと BPM · 拍子とグルーピング:リズムの概念と、推定器が置いている前提
- キー検出 · コード認識 · クロマ特徴量:和声をどう測っているか
- セクションと構成 · メロディとピッチ · オンセット検出:構成、旋律、打点
- スペクトログラムと STFT · メル・MFCC・音色:すべての土台になる共通層
- JavaScript 解析 API · JavaScript 特徴抽出 API:フィールド単位のリファレンス
- librosa 互換性:同じコアへのもう 1 つの入口
- ルーム音響解析:音楽ではなく録音空間の解析
- はじめに:導入と最初のファイルのデコード