Skip to content

音源分離 ​

音源分離は、1 つの録音から複数の音声信号を作る処理です。持続音とトランジェント(打撃的な成分)を分けるなら HPSS、録音から少数のラベルなしスペクトル成分を分解して取り出すなら NMF、複数チャンネルに同じマスクを使うなら linked NMF を選びます。これらの処理は楽器名を判定するものではなく、ボーカルだけを取り出せる保証もありません。

信号処理と学習済みモデルの違い ​

録音を複数のパートに分ける方法には大きく 2 つの系統があり、得意なことが違います。libsonare が実装しているのは 1 つ目だけです。

  • 信号処理による分離は、信号そのものの性質に基づく規則で分けます。スペクトログラム上で持続音は横線、打撃は縦線になる性質を使う方法(HPSS)や、スペクトログラムを少数の非負のスペクトル形状とその活性度に分解する方法(NMF)がこれにあたります。何も学習しないので、結果はモデルが過去にどんな音楽を見たかに左右されません。
  • 学習済みの分離モデルは、ミックスとその個別ステムの組を大量に使って訓練したニューラルネットワークです。声、ドラムキット、ベースがどう聞こえるかを学習し、名前の付いたステムを返します。完成したミックスからボーカル、ドラム、ベースを取り出す用途では、このページの方式を含むどの信号処理よりも、一般にずっときれいなステムが得られます。
信号処理(このページ)学習済みの分離モデル
出てくるもの構造上のパート。倍音成分と打撃成分、またはラベルのない成分ボーカル、ドラム、ベース、その他といった名前付きのステム
密なミックスで重なった楽器混ざったまま残る。ボーカルだけを取り出すルートはない多くの場合こちらが有利
再現性同じ入力と設定からは同じ出力が得られる。乱数は使わないモデルの重みと推論ランタイムに依存し、モデルの版が変わると結果も変わりうる
必要なものエンジンだけ。CPU で動き、ブラウザでも動くモデルの重み(数十〜数百 MB のことが多い)と推論ランタイム。速度のために GPU を使うことが多い
結果の理由マスクや w・h の行列から読み取れる訓練データから学習したもので、失敗する場面を予測しにくい

使い分けは次のとおりです。

  • ミックスから単独のボーカルやドラムのステムがほしい: 学習済みの分離モデルを使ってください。libsonare には含まれていません。
  • 解析の手がかり、再現できる分割、モデルをダウンロードしないブラウザ内処理がほしい: このページの方式が合います。打撃成分からビートを、倍音成分からキーを取るのが典型的な使い方です。
  • 両方を組み合わせる: 学習済みモデルで分離し、そのステムを libsonare に渡して解析、採譜、編集、ミキシングを行います。どの呼び出しも単なるサンプル列を受け取るので、どの分離器のステムでも使えます。バンドのミックスを採譜する前にステムが要る理由は 音声を MIDI に で説明しています。

方式を選ぶ ​

ルートは 3 つあり、それぞれ別の問いに答えます。

ルート入力出力入力に戻るか向いている用途
hpss(...)モノラル信号 1 本倍音成分と打撃成分の信号既定のソフトマスクなら戻る持続音と打撃的な成分の分離。たとえば打撃成分からビートを取る
decomposeStems(...)モノラル信号 1 本nComponents 本の信号と、w・h の行列戻るラベルのない成分を 1 つずつ聴いて確かめる
decomposeStemsLinked(...)同じ長さの複数チャンネル各成分をチャンネルごとに戻るマルチチャンネル録音のチャンネル間バランスと位相を保った成分
HPSS · FULL MIXIDLE
HPSS — 旋律と打楽器を分ける

スペクトログラムでは、持続する倍音成分は横方向の筋、立ち上がりの鋭い打撃成分は縦方向の筋になりやすくなります。HPSS はメディアンフィルタとマスクで、この構造を分けます。フルミックス・倍音成分・打撃成分を切り替えて試聴してください。楽器を個別に分離する処理ではないため、音程を持つ音の立ち上がりが打撃成分に入り、長く響くシンバルが倍音成分に入ることもあります。

レイヤー

デモは、ドラム入りのバンドのフレーズに対する倍音成分と打撃成分の分割です。フルミックスと 2 つの層を切り替えてみてください。持続する音程のある音は横方向の筋として倍音成分に残り、打点は短い縦方向の筋として打撃成分に残ります。これはスペクトルの形による 2 分割であって、複数楽器への分離ではありません。下の図は、2 つのメディアンフィルタがこの分割をどう作るかを示したものです。

HPSS: 2 つのメディアンフィルタと 2 つのマスク
振幅スペクトログラム時間 →周波数 →周波数方向のメディアン縦の筋を残す → 打撃成分部分音は列の中で 1 つだけ外れた値なので消える時間方向のメディアン横の筋を残す → 倍音成分打点は行の中で 1 つだけ外れた値なので消える倍音成分の出力打撃成分の出力ソフトマスク H² / (H² + P²) で各セルを分け合うので、2 つの出力を足すと入力に戻る
持続する 2 つの音と 4 つのドラムの打点です。時間方向のメディアンは横に伸びる部分音を残して打点を消し、周波数方向のメディアンは打点を残して部分音を消します。ソフトマスク(べき 2)は、2 つのフィルタ結果の大小比で各セルを 2 つの出力に分け合います。図ではスケッチに合わせて窓を 9 セルにしていますが、既定は 31 フレームと 31 ビンです。
typescript
import { init, hpss, hpssWithResidual, decomposeStems } from '@libraz/libsonare';

await init();

// 固定された軸での 2 分割:持続音かトランジェントか。
const { harmonic, percussive } = hpss({ samples, sampleRate });

// 同じ分割で、残差を表に出す。
const hard = hpssWithResidual({ samples, sampleRate, hardMask: true });

// 教師なしの成分分解。どの成分もそのまま聴ける。
const { components, w, h } = decomposeStems({
  samples,
  sampleRate,
  nComponents: 4,
  maskPower: 2, // Wiener 型。分離は強いが、共有する倍音でアーティファクトが増える
});
typescript
import { hpss, hpssWithResidual, decomposeStems } from '@libraz/libsonare-native';

const { harmonic, percussive } = hpss({ samples, sampleRate });
const hard = hpssWithResidual({ samples, sampleRate, hardMask: true });
const { components, w, h } = decomposeStems({ samples, sampleRate, nComponents: 4, maskPower: 2 });
python
import libsonare as sonare

split = sonare.hpss(samples, sample_rate=sample_rate)
harmonic, percussive = split.harmonic, split.percussive
hard = sonare.hpss_with_residual(samples, sample_rate=sample_rate, hard_mask=True)
stems = sonare.decompose_stems(samples, sample_rate=sample_rate, n_components=4, mask_power=2)
components, w, h = stems["components"], stems["w"], stems["h"]
bash
sonare hpss recording.wav -o separated.wav
sonare hpss recording.wav --hard-mask --with-residual -o hard.wav
sonare decompose-stems recording.wav --n-components 4 --mask-power 2 -o stems.wav

HPSS の nFft は 4 以上の偶数です。混合基数 FFT なので、2 のべき乗である必要はありません。hopLength は (0, nFft / 2] の整数、両方のメディアンカーネルは 524287 以下の正の奇数を指定します。倍音側のカーネルは時間方向の STFT フレーム数、打撃側のカーネルは周波数ビン数で数えた幅です。22050 Hz で nFft 2048、hopLength 512 なら、既定の 31 はそれぞれ約 0.72 秒と約 330 Hz にあたります。31 フレームのメディアン(並べた中央の値で、短い外れ値を無視します)は、その半分強(約 0.36 秒)を超えて同じ場所にとどまる音を残し、それより短い出来事を消します。周波数方向の 31 ビンも同様に、約 165 Hz 以上に広がった出来事を残します。既定のソフトマスクでは、2 つのフィルタ結果の大小比でセルを分け合うため、これらは境界線ではなく傾向です。C++ で独自の STFT 設定を渡す場合は、winLength と窓関数の組み合わせによって重ね合わせに隙間ができないことも必要です。不正な設定は、周期的な音抜けを生む代わりに拒否します。

HPSSdecomposeStems(...)
分割を決めるもの固定された軸。時間方向に取ったメディアン(並べた中央の値で、短い外れ値を無視します)が持続音を、周波数方向のメディアンがトランジェントを残す非負値行列因子分解が、素材そのものから nComponents 個の反復するスペクトル形状を学習する
返るものhpss は倍音成分と打撃成分の 2 信号を返す。hpssWithResidual は残差も返す成分ごとの信号 1 本ずつと、成分行列 w・アクティベーション行列 h
ラベルあらかじめ分かっている無い。w と h を見てどの成分が何かを判断する
再構成既定のソフトマスクでは倍音成分と打撃成分の和が入力に戻る。ハードマスクでは残差が生じることがあるモデルにエネルギーがある領域でマスクの和が 1 になるため、成分を足すと入力に戻る
位相元のまま元のまま。複素スペクトログラムにマスクを掛けるので、各成分がそのまま聴ける

ソフトマスク・ハードマスク・Wiener 型マスク

マスクは、時間・周波数のセルごとに、成分がそのセルをどれだけ受け取るかを表す数値です。

  • ソフトマスク(HPSS の既定):0 から 1 の割合です。HPSS は各セルの H^p / (H^p + P^p) を倍音側に、残りを打撃側に渡します。H と P はメディアンフィルタを通した 2 つのスペクトログラム、p はマスクの指数(既定 2)です。
  • ハードマスク(hardMask: true):各セルをまるごとどちらか一方に割り当てます。分け方としてはすっきりしますが、隣り合うセルで切り替わる箇所がアーティファクトとして聞こえることがあります。
  • NMF の maskPower:成分 k は各セルの (W_k·H_k)^p / Σ_j (W_j·H_j)^p を受け取ります。1 なら振幅の比に従い、2(Wiener 型)では強い成分がより大きな割合を取るため、分離は強くなる代わりに成分が重なる場所でアーティファクトが増えます。

w と h の読み方 ​

NMF は振幅スペクトログラムを 2 つの非負行列の積で近似します。各成分は、時間とともに大きくなったり小さくなったりする 1 つの固定したスペクトル形状です。

行列形(行優先)各列・各行の中身読み方
wnBins × nComponents(nBins = nFft / 2 + 1)列 k:成分 k のスペクトルのテンプレート。周波数ビンごとに 1 つの値エネルギーのある場所。低域のピークはベースやキック、等間隔に並ぶピークは音程のある音、幅広い分布はノイズやシンバルを示唆します
hnComponents × nFrames行 k:成分 k のアクティベーション。フレームごとに 1 つの値鳴っている時刻。鋭いスパイクは打点、長い平坦部は持続音です

フレーム t の周波数ビン f の値は Σ_k w[f][k] × h[k][t] で近似されます。ビン f は f × sampleRate / nFft Hz、フレーム t は t × hopLength / sampleRate 秒です(スペクトログラムと STFT)。beta は当てはまりの測り方を選びます。2 は差の 2 乗(Frobenius ノルム)で、絶対誤差の大きい大音量の部分が支配的になります。1 は Kullback-Leibler ダイバージェンスで、比率の誤差を測るため、小さな成分の細部を相対的に重く扱います。

以下の NMF デモでは 4 つの成分を個別に再生できます。操作中の処理時間を抑えるため、反復回数は 30 回です。成分に楽器名は付いていません。

A/B PROCESS · NMFIDLE
NMF 分解 — 学習した4つの成分

NMF はミックス内で繰り返されるスペクトルの形を学習し、名前の付いていない4つの成分を返します。各成分とフルミックスを聴き比べてください。1つの成分に複数の楽器が含まれることがあり、成分番号は楽器名を表しません。このデモは30回反復し、元の位相と処理後のレベルを保ちます。成分ごとの音量合わせは行いません。

比較
成分

主な既定値を挙げます。メディアンフィルタのカーネルはどちらも 31 で、既定のソフトマスクでは 2 つのマスクの和がすでに 1 になるため hpssWithResidual(...) の残差は無音です。どちらの成分にも割り当てられなかった帯域を残差として得たい場合は hardMask: true を渡します。decomposeStems(...) と decomposeStemsLinked(...) の NMF の既定値は同じで、成分 4 個、nFft: 2048、hopLength: 512、反復 100 回、beta: 2(Frobenius。Kullback-Leibler なら 1)、init: 'random'、maskPower: 1(振幅比)です。どちらのステム関数も sampleRate を省略すると 22050 を使うため、録音の実際のレートを渡してください。

マルチチャンネルの録音には decomposeStemsLinked(...) を使います。各チャンネルの振幅スペクトログラムを平均して 1 つの NMF モデルと成分マスクを作り、そのマスクを各チャンネルの元の複素スペクトログラムへ同じまま適用します。各時間・周波数ビン内のチャンネル間のレベル差と位相差を保ち、左右で別々のマスクを選ぶことを避けます。ただし、各成分に残る内容はフルミックスと異なるため、分離した成分のステレオの広がりや定位がフルミックスと同じになる保証はありません。

入力は同じ長さの Float32Array を少なくとも 1 つ渡し、チャンネル数を 64 以下にします。結果の components[k][c] は成分 k のチャンネル c の信号です。w、h、sampleRate の意味は decomposeStems(...) と同じです。1 チャンネルだけ渡すと、同じオプションの decomposeStems(...) とビット単位で一致します。

typescript
import { init, decomposeStemsLinked } from '@libraz/libsonare';

await init();

const linked = decomposeStemsLinked({
  channels: [leftChannel, rightChannel], // 同じ長さの Float32Array
  sampleRate,
  nComponents: 4,
});
const firstLeft = linked.components[0][0];
const firstRight = linked.components[0][1];
console.log(linked.w.length, linked.h.length);
typescript
import { decomposeStemsLinked } from '@libraz/libsonare-native';

const linked = decomposeStemsLinked({
  channels: [leftChannel, rightChannel], // 同じ長さの Float32Array
  sampleRate,
});
const firstLeft = linked.components[0][0];
const firstRight = linked.components[0][1];
python
import libsonare as sonare

linked = sonare.decompose_stems_linked(
    [left_channel, right_channel], sample_rate=sample_rate, n_components=4
)
first_left = linked["components"][0][0]
first_right = linked["components"][0][1]
print(linked["w"].shape, linked["h"].shape)
bash
# CLI はモノラルの `decompose-stems` だけを提供します。
# 複数チャンネルで 1 つの NMF モデルを共有する場合はライブラリ API を使います。

どのルートも、密なミックスからボーカルだけを取り出すものではありません(信号処理と学習済みモデルの違いを参照)。得意なのは後段の推定器の仕事を楽にすることです。打撃成分でビート追跡、倍音成分でクロマとキー、リードが分離できた成分でピッチ追跡、またはステレオの定位を保ったマルチチャンネル処理、といった使い方になります。

関連ページ ​

楽曲解析、音声から音符へ、メロディとピッチ