音の立ち上がり検出
ChucKの音響分析機能を使って何かできないか調べたところ、音の立ち上がり検出が面白そうだったのでやってみることにした。
概要
音の立ち上がり検出([英]onset detection)は、与えられた音響信号の中から、個別の音が鳴り始めた時点を見つける処理である。音楽の場合はそれぞれの音符の始まりを検出する処理で、応用としては音楽信号の情報抽出や検索、自動転写、圧縮にも使われることがあるらしく、"onset detection"で探すと結構な数の論文が見つかる。
後述する短くて良い論文を見つけたので、それを参考にしてほぼリアルタイム(オンライン)に処理するプログラムを書いた。
立ち上がりを検出したら出力用の小さなウィンドウ内部が点滅する。このウィンドウ自体はProcessingで表示していて、ChucKで検出=>OSCでProcessingに通知という仕組み。
結果
ごく単純な入力とそれに対する出力の様子。
大体上手く検出できているけれど、6番目の音色の場合には立ち上がりではないところも立ち上がりと判定している*1一方で、7番目の音色の場合には立ち上がりを判定できていないところもある*2。
当然だが音が揺れていたりすると正確な検出は難しくなる。
音楽の場合は楽器が複数あって音が重なったりしているとまずそれを識別・分離する必要があるのではないかと思うが、そんな高度なことにはとても対処できない。自分のプログラムは。
というわけで(?)ある程度の複雑性を持ちながらも一種類の楽器のみが使われる音楽としてドビュッシーの月の光(録音)でやってみた。これでもハードルを上げすぎた感はある。
開始15秒くらいまで見ると結構いいのでは、と期待させられるが、その後は人間にとっては非常に明解に思える立ち上がりを検出できないところも多い。特に低音域はほとんど失敗する。検出方法で使うパラメータを弄れば改善するかもしれないが、手動ではやりたくない。チューニング大変そう。
方法とプログラム
立ち上がりの検出は次のような流れで行われることが多いらしい*3。
検出関数に変換する部分も極大値を選択する部分も様々な方法が提案されている。
音響信号
↓ (立ち上がりを検出しやすい信号に変換)
検出関数
↓ (検出関数から閾値を超える極大値を選択)
立ち上がり位置
今回は参考文献2を元に、劣化バージョンをChucKで実装した。検出関数への変換については、参考文献2にも複数の方法が紹介されているが、spectral fluxを使う方法を採用した。理由はChucKのFluxというユニットアナライザがそのまま使えるから。一番精度が良いというわけではない。
ChucKのプログラム。ここではSndBufに音響信号を全部読み込んでいるが、実際には再生している時点より先の信号を必要としない検出方法なので、adcに換えてマイクからリアルタイムに音を入力することもできる(ノイズ乗って精度落ちるだろうけど)。
1024 => int SIZE; SIZE/2 => int HOP; 20 => int ALOUD; 5 => int DF_CAP; 5 => int TH_CAP; .1 => float TH_ALPHA; 1.7 => float TH_LAMBDA; SndBuf buf => Gain g => dac; buf => FFT fft =^ Flux flux => blackhole; fft =^ RMS rms => blackhole; "input.wav" => buf.read; SIZE => fft.size; Windowing.hann(SIZE) => fft.window; fun void detect() { OscSend send; send.setHost("127.0.0.1", 12000); send.startMsg("/bang, f"); int idx; float buf[DF_CAP]; Threshold th; while (HOP::samp => now) { rms.upchuck().fval(0) => Std.rmstodb => float crms; flux.upchuck().fval(0) => float cflux; cflux - th.getThreshold(cflux) => float df; df => buf[idx]; (idx + 1) % DF_CAP => idx; int maxIdx; float max; for (int i; i < DF_CAP; i++) { if (buf[i] > max) { i => maxIdx; buf[i] => max; } } (idx + DF_CAP - 2) % DF_CAP => int pre2; if (max > 0 && maxIdx == pre2 && crms >= ALOUD) { send.startMsg("/bang, f"); send.addFloat(crms); } } } class Threshold { int idx; float sum; float buf[TH_CAP]; float tmp[TH_CAP]; fun float getThreshold(float cflux) { (-buf[idx] + cflux) +=> sum; cflux => buf[idx]; (idx + 1) % TH_CAP => idx; float t, median; for (int k; k < TH_CAP; k++) buf[k] => tmp[k]; int j; for (1 => int i; i < TH_CAP; i++) { for (i => j; j > 0 && tmp[j] < tmp[j - 1]; j--) { tmp[j] => t; tmp[j - 1] => tmp[j]; t => tmp[j - 1]; } } if (TH_CAP % 2 == 1) tmp[TH_CAP / 2 + 1] => median; else (tmp[TH_CAP / 2] + tmp[TH_CAP / 2 + 1]) / 2 => median; return TH_LAMBDA * median + TH_ALPHA * sum / TH_CAP; } } spork ~ detect(); buf.samples()::samp => now;
Processingのプログラム。
import oscP5.*; import netP5.*; OscP5 osc; int bgcol; PGraphics pg; void setup() { frame.setAlwaysOnTop(true); frameRate(50); size(150, 150); osc = new OscP5(this, 12000); pg = createGraphics(130, 130, P2D); noLoop(); } void draw() { if (bgcol < 0) { bgcol = 0; noLoop(); } pg.beginDraw(); pg.background(100 + bgcol, 100 + bgcol, 0); pg.endDraw(); image(pg, 10, 10); bgcol -= 5; } void oscEvent(OscMessage msg) { if (msg.checkAddrPattern("/bang")) { float rms = msg.get(0).floatValue(); bgcol = (int)rms * 4; loop(); } }
久々にChucKの新しい機能を使ったけど、題材も面白かったし良かった。
参考文献
- J.P. Bello, L. Daudet, S. Abdallah, C. Duxbury, M. Davies, and M.B. Sandler:
"A tutorial on onset detection in music signals", IEEE Transactions on Speech and Audio Processing, 13(5):Part 2, pp. 1035-1047 (2005). - P. Brossier, J. P. Bello, and M. D. Plumbley: "Real-time temporal segmentation of note objects in music signals" Proceedings ICMC 2004, pp. 458-461 (2004).
他には以下のブログの一連の記事は、音響信号の初歩から立ち上がり検出のプログラムまで順を追ってかなり詳しく書いてある。詳しすぎて長いので全部読んでない。