Skip to content

音源分離 ​

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

方式を選ぶ ​

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

A/B PROCESS · HPSSIDLE
ステム分解 — ミックスから打撃成分を取り出す

持続するパッドの和音の上に、拍ごとに鋭い広帯域の打点を重ねたクリップです(フルミックス)。ここで適用しているのは HPSS による分解で、B 側はその打撃成分だけです。打点は短い縦方向のイベントとして残り、パッドの安定したスペクトル線は取り除かれます。つまり 2 分割のうち片方のステム(打撃成分)であって、複数ステムへの完全な分離ではありません。平均スペクトルを重ねて表示するので、分割で何が残ったかが分かります。Compare を切り替えて、ミックスと取り出したステムを聴き比べてください。

比較

上のデモは倍音成分と打撃成分の分割です。パッドの和音の上に拍ごとの広帯域の打点が乗っており、B 側はその打撃成分だけです。持続するスペクトル線は押し出され、打点は短い縦方向のイベントとして残ります。これは 2 分割の片側であって、複数楽器への分離ではありません。

typescript
import { hpss, hpssWithResidual, decomposeStems } from '@libraz/libsonare';

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

以下の 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(振幅比)です。decomposeStemsLinked(...) は 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 を使います。

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

関連ページ ​

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