tuneR を使ったMFCCの計算

ほぼ自分用の備忘録←

tuneR で MFCC を求める手順。バージョンは 1.2.1。

library(tuneR)

# wavファイル読み込み
# mp3ファイルの場合は readMP3()を使えばよい
wobj <- readWave(filename)

# melfcc() は現行では mono 音声しか扱えないので必要に応じて変換
wobjm <- mono(wobj)

mfcc <- melfcc(wobjm, 
           numcep = 20, 
           nbands = 36,  
           wintime = 0.023,
           hoptime = 0.023)

nbands はメルフィルタバンクの数。numcepは、離散コサイン変換した結果、低周波成分から数えていくつ分の係数を取得するかを指定。wintimeは1フレームの長さ(ms), hoptimeは、フレーム間の時間間隔(ms)。

結果は (フレーム数)x(numcep) の行列で返ってくる。1列目は各フレームの平均エネルギーを表し、特徴を表すのに有用ではないので切り捨てて19次元にする。

Zero-crossing rate (ZCR) を使った音楽解析

音声認識や音楽解析によく使われる指標として Zero-crossing rate (ZCR) というのがあるそうだ。これは、音声の波形を描いたとき、波が中央より上(正)から中央より下(負)に、またはその逆に変化する頻度を数えて、その頻度により音声の特徴を表すというもの。ZCRが大きいほどより noisy な音声と捉えられるらしい。


この ZCR を使って様々なジャンルの音楽で分析を行った実例が、Elias Pampalk 氏の Computational Models of Music Similarity and their
Application in Music Information Retrieval という論文で紹介されている。面白そうな上、案外簡単に実装できそうな気がしたので、オイラも早速 perl で作ってみた。


(ソースは GitHub に置いておきました)


今回は分析方法として、楽曲の中央から前後30秒を抜き出し、その間に波が正から負、またはその逆に変化した点の数をカウント。1ms あたりの発生数を最終的な結果とした。

使用した楽曲は、オイラの手持ちの曲から適当に10曲ほど。事前に wav(44.1kHz, 16bit, mono)形式に変換しておいた。


結果は以下の通り。ZCRの小さい順に並べてある。

曲名 アーティスト ZCR
日々 吉田山田 1.21
Bossa Nova at 2:00 AM TOMOYOSHI NAKAMURA QUARTET 1.51
Around Forty Blues TOMOYOSHI NAKAMURA QUARTET 2.21
ごはんはおかず 放課後ティータイム 3.02
divine intervention fhana 3.39
Big City Bright Lights ArtOfficial 3.50
すぱそにっ すーぱーそに子 3.63
S・M・L☆ アフィリア・サーガ 4.16
Believe -天真爛漫 Ver.- すずみ 4.46
Skywalker Collioure 4.47
Brave your truth Daisy×Daisy 5.53


曲名についているリンクの飛び先で、楽曲の一部のみだが試聴することができる。実際に聞きながらZCR値を比較してみると効果が分かりやすいかも。(*1)


ZCR が noisy さを表す指標と考えると、概ね合っているかなぁ、という印象(気のせい?)。吉田山田さんの「日々」とか純朴な曲だし、ZCR が低いのは納得。先に紹介した Elias Pampalk 氏の論文だと、ジャズはおおむね低くなる傾向があるとのことだったけど、上記の実験でも TOMOYOSHI NAKAMURA QUARTET さんの2曲(ジャズ)はともに低い ZCR となっていた。


意外だったのが、Collioure さんの Skywalker。結構爽やかなサウンドで、ZCRも低くなるんじゃないかと予想してたのだが、結果は10曲の中で上から2番目。パーカッションが強いからかなぁ。


あと今回は試せなかったのだが、クラシック音楽はあまり良い結果を得られないらしい。そもそも ZCR の限界として、音の高低にも左右されてしまう(高音=周波数が高い=ZCRが高くなる)という点がある。なので ZCR 単体ではなく、他の手法とも併用するのが良いようだ。


とはいえ、実装が案外簡単な割に良い結果が得られた気がする。時間があったら今回試せなかったジャンルの曲も試してみたい。



(*1)ステマ

CPAN Author になった話

先日、ようやく CPAN Author になった。


Data::HandyGen::mysql という、テストデータを効率よく生成するためのモジュールを作って、今年の2月に公開した。はじめて CPAN モジュールを公開するという体験は実にドキドキものである。少年の心が一瞬、蘇った気がする。←
それはそうと、実際に公開するにあたって躓いた点が多々あったので、今回はそのあたりを振り返ってみたいと思う。これから CPAN Author になろうという人たちに少しでも参考になれば。


なお、CPAN へのモジュール登録手順については、ささたつさんのこの記事を参考にさせていただいた。日頃、技術的なことで困ってググるとささたつさんの記事にたどり着くことが実に多い(笑) いつもありがとうございます。

PAUSE ID はすぐには発行されない

CPAN にモジュールを登録するには、まず PAUSE に開発者としてアカウントを登録する必要がある。ただ、ここからの登録申請を承認する作業は人手で行われているらしい。PAUSE のサイトには、

  • 最大で3週間は待って欲しい
  • だいたい1週間以内には登録されると思う


と書かれている。今回オイラは日本時間の 2月15日 夜2:23 に登録申請して、その日の4:15に登録完了のメールが届いた。ほぼ2時間ということでかなり早く処理していただけた。ただ上記のただし書きもあるので、近日中にモジュールを公開する予定であるなら早めに申請しておくと良いかも。

Gravatar に登録する「前に」 PAUSE のメール転送設定を

これ、ささたつさんのサイトにもちゃんと書いてあったのにオイラもハマった。要注意(笑)


PAUSE の登録が完了すると、cpan.org ドメインの新たなメールアドレスが自分用に発行される(egawata@cpan.org みたいなの。以下 xxx@cpan.org)。ただしこのアドレス宛のメールは直接受信することはできず、受け取るためには事前に転送先を登録しておく必要がある。この設定は PAUSE 登録完了後、PAUSE サイトの "Edit Account Info" で設定できる。

  1. 個人のメアドを Secret emaili address... (もしくは公開してよいなら Publicly visible email address) の欄に設定
  2. "The email address xxx@cpan.org should be configured to forward mail to ..." で、xxx@cpan.org 宛のメールの転送先を指定。以下の3つが指定可能。
    • my public email address
    • my secret email address
    • neither nor


一番下の "neither nor" が初期設定。そしてこの設定のときは、どこにも転送されず、おそらく捨てられてしまう。


さて問題はここだ。PAUSE のアカウント申請から登録完了まで時間が空くので「そうだ、今のうちに Gravatar の登録をやってしまおう」なんて、時間効率最適化図ってる俺カッケー的な発想が沸き起こってしまいがちだ。が、これは絶対にやってはいけない


CPAN サイトで表示される自分のアイコンは、先ほど触れた xxx@cpan.org のメアドで Gravatar に登録する必要があるのだ。そしてそのメアド宛に届く確認メールを受け取れないと、アカウント登録をいつまでも完了させられない。
しかもメールが届かないからと再度同じメアドで登録しようとすると、アカウントが重複している旨のエラーが出て登録できない。もしかしてもう一回別の名前で PAUSE へのアカウント登録からやらないとダメ!? なんて焦ってしまう。が、そんな時は慌てず、パスワードリセットのリクエストをするとよい。


いずれにしても、Gravatar の登録は、メール転送設定をしたあとにゆっくりやるのが一番だろう。

PrePAN は投稿後少し待ったほうがよい(?)

PAUSE のサイトでは、特に初めてモジュールをアップロードする開発者に対し、PrePAN というサイトでモジュールのフィードバックを貰うことを強く推奨している。ここでモジュール名と、モジュールの機能について投稿すると、先人から意見が貰えるというものだ。特にモジュールの命名の是非について意見を貰えることが多いようだ。


一応、推奨されているので素直に従っておいた。3日待ったところ、特に何もコメントがなかったので、だったら大丈夫かなぁと思い、公開に踏み切った。
ちなみに過去の投稿を見る限り、モジュールの命名についてコメントが付くのはだいたい2つに一つくらいの割合で、最初にコメントが付くのは遅くとも投稿後3日以内が多いようだ。ここでコメントがつかないようなら、問題ないと考えてしまっていいだろう(多分)。

テストはできるだけ多くの環境で!

これが今回の一番の反省点。オイラが使っている環境は Fedora がメインで、あとは仮想環境で Ubuntu を使うくらい。一応 Windows マシンも持ってはいるのだが、特別の用途(*1)がない限り、ほとんど立ち上げることはない。


ということで、今回うっかり Windows 環境でのテストを忘れてしまったのだ。


その結果…ごらんの有様だよorz


このレポートを受け取って慌てて Windows マシンを立ち上げテストを走らせてみる…
いやぁ、こけるこけるwwwww


とりあえず速攻で対応して Windows でもちゃんとテスト通るように修正したのだが、テストスクリプトだけ更新してバージョン上げるのもどうかなぁと思い、そのままにしてある。次に機能追加するときについでにリリースしよう。


(2014/03/30 更新) v0.0.2公開しました。Windowsでもテスト通るようになりました。


ともあれ、テストは可能な限り多くの環境で行うべし。特に Windows でのテストは忘れがちなので気をつける(*2)


(*1)「特別の用途」が何であるかを突っ込むのは無粋だと思う。
(*2)オイラだけか。



というわけで、反省点をつらつらと。
まぁでも、公開してみて分かったのは「モジュールをCPANに公開するのは案外怖くない」ということ。そして一度公開すると、次は何を作ろうかとか色々考えて楽しくなる。どんなモジュールでもいいから、少しでも他の人の役に立ちそうだと思ったら思い切って公開してしまうと良いんじゃないかな。

機能追加

機能「追加」は決して機能「改良」ではない。

新しい機能が使えるようになるという意味で「改良」だが、システムの複雑度が増すという意味で「改悪」。開発運用のコストも上がるし、ユーザからしても、機能全体を理解するのが難しくなる。

逆に、初期に導入したけど実際に運用しはじめたら実は要らなかったって機能があるなら、どんどん言ってほしい、って個人的には思う。要らない機能を削除することで、開発・運用がやりやすくなり、それは機能「改良」になる。

F10で右クリックメニューが表示されないようにする

Fedora17上で gnome-terminal を使っていたら、F10キーで右クリックメニューが表示されてしまう。
vimキーバインドで F10 を使っているのでこれは困った。

一応、メニューの「編集」→「キーボードショートカット...」で F10 を無効にする設定は存在する。これを「有効」に設定していると F10 キーを押した時点で上部のメニューが開く。で、無効にするとその挙動はなくなるのかなぁと思ったのだが、上部のメニューが開かない代わりに、マウスカーソルの位置に右クリックメニューが表示されてしまう。一応、vimキーバインドも機能はしているようだが、いちいちメニューが表示されるのはうっとうしい。

いろいろ調べてみたところ、Gnome3の設定ファイルを追加すればいいようだ。

~/.config/gtk-3.0/gtk.css というファイルを新規に作成し、以下の内容を記述。

 @binding-set NoKeyboardNavigation {
    unbind "<shift>F10"
 }
 
 * {
     gtk-key-bindings: NoKeyboardNavigation
 }

これでメニューは表示されなくなった。めでたしめでたし。

(参考) https://bbs.archlinux.org/viewtopic.php?id=129872

libxml2 で XMLTextReader を使ってみる

XML文書をパースする機能が必要になったので、libxml2 を使ってみた。
今までオイラは XML のパースは軽量言語(Perl、PHPなど)でしかやったことがなかった。libxml2をそのまんま使うとものすごく面倒なんだろうなぁと若干恐れおののいていたのだが、いざ使ってみると案外そうでもない。(少なくともフォーマットが分かっているXMLから必要な項目を取り出すだけなら)


ということで、XML文書から商品名と価格を取り出すという簡単なサンプルを作ってみた。

(例によってサンプルコード全体は github にあげてあります)


サンプルデータ(sample.xml)はこんな感じ。

<?xml version="1.0" encoding="UTF-8"?>
<fruits>
    <fruit>
        <item>りんご</item>
        <price>150</price>
    </fruit>
    <fruit>
        <item>みかん</item>
        <price>250</price>
    </fruit>
    <fruit>
        <item>メロン</item>
        <price>1000</price>
    </fruit>
</fruits>


で、これを処理してみる。プログラム側のざっくりとした流れは以下のようになる。

    xmlTextReaderPtr reader;
    int ret;

    //  Readerの作成
    reader = xmlNewTextReaderFilename("./sample.xml");

    //  次のノードに移動 
    ret = xmlTextReaderRead(reader);
    while (ret == 1) {
        //  現在のノードを処理(processNodeは別途定義)
        processNode(reader);

        //  次のノードに移動
        ret = xmlTextReaderRead(reader);
    }

    //  Reader のすべてのリソースを開放
    xmlFreeTextReader(reader);


ここで言う「ノード」とは、開始タグ・終了タグやテキストなどのこと。例えば

<item>りんご</item>

このようなXMLは、

  1. <item>
  2. りんご
  3. </item>


の3つのノードとして認識される。

    //  Readerの作成
    reader = xmlNewTextReaderFilename("./sample.xml");

xmlNewTextReaderFilename() は、ある XML リソースを読み込むためのReaderを作成する関数。引数に指定するのは、処理対象のリソースURI。今回はファイル名を指定したが、http://〜 で始まる文字列を指定すればネット上からリソースを取得することも可能。

    //  次のノードに移動 
    ret = xmlTextReaderRead(reader);
    while (ret == 1) {
        //  現在のノードを処理(processNodeは別途定義)
        processNode(reader);

        //  次のノードに移動
        ret = xmlTextReaderRead(reader);
    }

xmlTextReaderRead() は、XMLストリーム上の次のノードへ読み進める関数(初回は最初のノード)。戻り値は

  • 1: 正常終了
  • 0: 読み進めるべきノードがない
  • -1: エラー

よって、1 を返している間は読み進めたノードの処理を繰り返す。


処理本体は processNode() という別関数で定義した。

//  1つのノードを処理する
void processNode(xmlTextReaderPtr reader)
{
 …


現在 reader がポイントしているノードのさまざまな情報を取得する関数が用意されている。例えば以下のようなものがある。

項目名 取得用関数 概要
NodeType xmlTextReaderNodeType() ノードの種類
Name xmlTextReaderName() ノード名
Value xmlTextReaderValue() Textノードの場合、そのText
Depth xmlTextReaderDepth() XMLツリー内の階層の深さ(root=0)

NodeType は整数の値で表される。libxml/xmlreader.h では以下の18種類の値が定義されている。

typedef enum {
    XML_READER_TYPE_NONE = 0,
    XML_READER_TYPE_ELEMENT = 1,
    XML_READER_TYPE_ATTRIBUTE = 2,
    XML_READER_TYPE_TEXT = 3,
    XML_READER_TYPE_CDATA = 4,
    XML_READER_TYPE_ENTITY_REFERENCE = 5,
    XML_READER_TYPE_ENTITY = 6,
    XML_READER_TYPE_PROCESSING_INSTRUCTION = 7,
    XML_READER_TYPE_COMMENT = 8,
    XML_READER_TYPE_DOCUMENT = 9,
    XML_READER_TYPE_DOCUMENT_TYPE = 10,
    XML_READER_TYPE_DOCUMENT_FRAGMENT = 11,
    XML_READER_TYPE_NOTATION = 12,
    XML_READER_TYPE_WHITESPACE = 13,
    XML_READER_TYPE_SIGNIFICANT_WHITESPACE = 14,
    XML_READER_TYPE_END_ELEMENT = 15,
    XML_READER_TYPE_END_ENTITY = 16,
    XML_READER_TYPE_XML_DECLARATION = 17
} xmlReaderTypes;

今回のサンプルでは、1 = 開始タグ、3 = テキスト、15 = 終了タグ の3つのみ使用する。


さて、さっそく現在処理中のノードタイプとノード名を取得してみる。

    xmlElementType nodeType;
    xmlChar *name, *value;

    //  ノード情報の取得
    nodeType = xmlTextReaderNodeType(reader);       //  ノードタイプ
    name = xmlTextReaderName(reader);               //  ノード名
    if (!name)
        name = xmlStrdup(BAD_CAST "---");

name は常に取得可能とは限らず、NULL が返る場合もある。今回は取得出来なかった場合 "---" という文字列を代わりに代入することにした。BAD_CAST というのは、libxml2 で定義されているマクロで、通常の文字列を xmlChar* 型にキャストしても安全と分かっている場合のキャスト用に使用する。

    if (nodeType == XML_READER_TYPE_ELEMENT) {              //  開始
        if ( xmlStrcmp(name, BAD_CAST "item") == 0 ) {
            state = STATE_ITEM;

        } else if ( xmlStrcmp(name, BAD_CAST "price") == 0 ) {
            state = STATE_PRICE;
        }

ノードが開始タグである場合は、ノード名を調べる。"item" または "price" というノードなら変数 state を変更して、現在それらのノードの中にいることが分かるようにする。

    } else if (nodeType == XML_READER_TYPE_END_ELEMENT) {   //  終了
        state = STATE_NONE;

ノードが終了タグである場合は、"item" または "price" タグから抜けたということを表すため state を変更する。"item" "price" 以外のタグから抜けた場合でも無駄に処理されるが気にしない。w

    } else if (nodeType == XML_READER_TYPE_TEXT) {          //  テキスト
        //  テキストを取得する
        value = xmlTextReaderValue(reader);

        if (!value)
            value = xmlStrdup(BAD_CAST "---");

        if ( state == STATE_ITEM ) {
            printf("品名: %s\n", value);

        } else if ( state == STATE_PRICE ) {
            printf("価格: %s\n", value);
        }

ノードがテキストノードだった場合は、現在どのノード内にいるかを調べ、適切な文字列を表示する。テキストノードの値を取得するには xmlTextReaderValue() を使用する。

        xmlFree(value);
    }

    xmlFree(name);
}  

xmlChar* 型の変数を宣言し、かつ xmlTextReader〜() 関数を使って文字列を取得できた場合は、使用後に xmlFree() で適切に開放しなければならない。

    //  Reader のすべてのリソースを開放
    xmlFreeTextReader(reader);

すべての XML パース処理が終わったら、Reader のリソースをすべて開放する。


コンパイルは以下のとおり行う。ライブラリやインクルードファイルのパス指定は、xml2-config を使うと楽。

gcc -Wall -o xmlread xmlread.c `xml2-config --cflags --libs`


実行結果

-----------------------
品名: りんご
価格: 150円
-----------------------
品名: みかん
価格: 250円
-----------------------
品名: メロン
価格: 1000円
-----------------------