2013年5月18日土曜日

Androidで音声解析!Visualizerによる高速フーリエ変換(FFT)


今回は、音の周波数を解析して音階を特定するというのをやってみたいと思います。

音階の特定には、時間軸を周波数軸に変換するフーリエ変換という処理で周波数解析を行います。

\hat{f}(\xi) := \int_{-\infty}^{\infty} f(x)\ e^{- 2\pi i x \xi}\,dx

数式を載せてみたものの、これが本当にフーリエ変換の式なのかすらよく分かりません。
数学的な知識はまったく無いので、フーリエ変換についての説明や考察は一切しません。いや、出来ません。

基本的に勘で進めているので、間違った記述もあるかもしれませんが、取りあえず動いたので完全に間違いという事ではないでしょう。


まずはシンプルに440Hz(Aの音)のsin波を鳴らして、それが440Hzであることが分かればゴールとします。


フーリエ変換を行うには、波形を表示したりといったことにも活用出来るandroid.media.audiofx.Visualizerを利用します。
android.media.audiofxはAPIレベル9(Android2.3)で追加されたパッケージで、イコライザ機能などを簡単に実装するものです。


public Visualizer (int audioSession)

 int audioSession 分析対象のMediaPlayerやAudioTrackのセッションID



流れとしては、Visualizerのコンストラクタに解析したいMediaPlayerなどのセッションIDを渡してやり、setCaptureSize()メソッドでキャプチャサイズを設定し、setDataCaptureListener()でリスナーを登録します。

キャプチャサイズを設定するときに利用するVisualizer.getCaptureSizeRange()で返ってくる配列はlengthが2で、index[0]は128、index[1]は1,024です。
つまり、キャプチャサイズは128か1,024しか選択出来ないようです。
129や2,048など別の数字を入れてもエラーにはなりませんが、VisualizerのgetCaptureSize()で確認すると、1,024になっていたので、自動で修正されているようです。

public int setDataCaptureListener (Visualizer.OnDataCaptureListener listener, int rate, boolean waveform, boolean fft)

Visualizer.OnDataCaptureListener listener 登録するリスナー
int rate         キャプチャ更新のレート
boolean waveform trueならonWaveFormDataCapture()が呼ばれる
boolean fft      trueならonFftDataCapture()が呼ばれる


キャプチャ更新レートの最大値はVisualizer.getMaxCaptureRate()で20,000です。

登録するリスナーはVisualizer.OnDataCaptureListenerインターフェースを実装したもので、以下二つの抽象メソッドがあります。

public abstract void onFftDataCapture (Visualizer visualizer, byte[] fft, int samplingRate)

public abstract void onWaveFormDataCapture (Visualizer visualizer, byte[] waveform, int samplingRate)


onFftDataCapture()がフーリエ変換後のデータを利用するもので、onWaveFormDataCapture()は生のWaveデータを利用するものです。


それではいよいよonFftDataCapture()についてみていきましょう。

まずドキュメントでVisualizerのgetFft (byte[] fft)部分を見ると、index[1]を除くと、byte配列には実数部と虚数部が交互に格納されているようなことが書いてあります。

ソースをちゃんと見ていませんが、onFftDataCapture()で渡されてくるbyte配列も恐らくこれと同じでしょう。

実数?虚数?という感じですが、複素数平面に拡張するには訳があります。

ある波を複数のsin波の合計で表現することがフーリエ変換なのですが、位相のズレを表現するためには虚数が必要なんです。
位相のズレたsin波をsinやcosをフル活用して表現する的な感じですかね。

はい。

あんまり良く分かりません。

まぁここで重要なのは、onFftDataCapture()のbyte配列の実数部がcos波、虚数部がsin波を表すってところです。

※という割には実数部を見ても虚数部を見ても同じところにピークが来ていました。また、ためしにcos波を混ぜてみたらまったく想定していない結果になり、謎は深まるばかりです。


ちなみにどの程度細かく分析が可能かに触れていませんでしたが、実はそんなに細かく分析出来ません。

サンプルレートが44,100Hzであれば、いわゆるナイキスト周波数は22,050Hz(44,100Hz / 2)で、キャプチャサイズが1,024であれば、分解能は21.5332031Hz(44,100Hz / 2 / 1,024)となります。

これはG♭3とG3ぐらいの差なので、音階を完全に特定するには十分とはいえない気がしますが、これが最大値なようなので仕方が無いです。
低周波になってくると音階の区別がつかなくなるってことでしょう。


つまり、サンプルレートが44,100Hzで、キャプチャサイズが1,024であれば、分解能は21.5332031Hzとなり、FFTの結果のbyte配列は恐らく以下のよう解釈すれば良いと思われます。

index[0]  実数部 0Hz~10.76660155Hz
index[1]  実数部 10.76660155Hz~21.5332031Hz
index[2]  実数部 21.5332031Hz~43.0664062Hz
index[3]  虚数部 21.5332031Hz~43.0664062Hz
index[4]  実数部 43.0664062Hz~64.5996093Hz
index[5]  虚数部 43.0664062Hz~64.5996093Hz




440HzのAの音であれば、以下のindex[41]にピークがくるということです。

index[41]  虚数部 430.664062Hz~452.1972651Hz

では実際にやってみましょう。
まずはFFTの結果を拾うところまで。


AudioTrack mAudioTrack;
Visualizer mVisualizer;

//サンプルレート
static int SAMPLE_RATE = 44100;
int bufSize = SAMPLE_RATE * 5;

//ストリームモードでAudioTrackを利用します
mAudioTrack = new AudioTrack(
  AudioManager.STREAM_MUSIC, SAMPLE_RATE,
  AudioFormat.CHANNEL_OUT_MONO,
  AudioFormat.ENCODING_DEFAULT,
  bufSize, AudioTrack.MODE_STREAM,
  420);//セッションID
mAudioTrack.play();


mVisualizer = new Visualizer(420);

// 1024
int captureSize = Visualizer.getCaptureSizeRange()[1];
mVisualizer.setCaptureSize(captureSize);


mVisualizer.setDataCaptureListener(
  new Visualizer.OnDataCaptureListener() {
   // Waveデータ
   public void onWaveFormDataCapture(
     Visualizer visualizer,
     byte[] bytes, int samplingRate) {
   }

   // フーリエ変換
   public void onFftDataCapture(
     Visualizer visualizer,
     byte[] bytes, int samplingRate) {

    //このbytesがFFT後のデータ
    //何度も呼ばれるのでこのデータを分析する

   }, Visualizer.getMaxCaptureRate(),
   false,//trueならonWaveFormDataCapture()
   true);//trueならonFftDataCapture()

mVisualizer.setEnabled(true);

byte[] audioData = new byte[bufSize];

// A単音
double freqA = 440;
double t = 0.0;
double dt = 1.0 / SAMPLE_RATE;
for (int i = 0; i < audioData.length; i++, t += dt) {
 audioData[i] = (byte) (Byte.MAX_VALUE * (Math.sin(2.0 *
   Math.PI * t * freqA)));
}
mAudioTrack.write(audioData, 0, audioData.length);


これでフーリエ変換後のデータを取得出来ました。
あとはピークがどこに来ているか確認し、分解能を元に周波数を特定すれば良いだけです。

ただし、結果を見てみると概ね正しく解析出来ますが、おかしな数字が出てくることもあります。

index[41] : 441.4306640625
index[41] : 441.4306640625
index[41] : 441.4306640625
index[73] : 785.9619140625
index[73] : 785.9619140625
index[141] : 1518.0908203125
index[169] : 1819.5556640625
index[123] : 1324.2919921875
index[41] : 441.4306640625
index[41] : 441.4306640625
index[41] : 441.4306640625
index[41] : 441.4306640625
index[49] : 527.5634765625
index[41] : 441.4306640625
index[149] : 1604.2236328125
index[149] : 1604.2236328125
index[41] : 441.4306640625
index[41] : 441.4306640625
index[41] : 441.4306640625

正しいindex[41]以外はバラバラですね。

虚数部しか見ていなかったので、位相のズレなどが正しく解釈出来ていないということでしょうか??

まぁちょっと勉強が必要ですね。

Read More...

2013年4月22日月曜日

テストも簡単!Intel XDKで書いたアプリをAndroidでテストする


前回エミュレータ上では、AndroidでもiPhoneでも動いたthree.jsのサンプルですが、今回は実際に実機でテストしてみます。

XDKのエミュレータの右上にある「App Tester」をクリックします。

作成したProjectがクラウドに転送されます。

転送完了。
右矢印をクリックすると手順の説明が出てきます。

ここからはappMobiのAndroidアプリ「app•lab」をインストールし、Android側で作業します。

アプリを起動し、「enter the lab」から先へ進みます。

「log in now」からブラウザを起動します。
どうやらはじめから画面の右上に表示されている「MY APPS」からでも同じく飛べるようです。

XDKに登録した際のIDとPWを入力してログインします。

先ほど転送したProjectを選択します。

「App Tester」を選択します。
「Test Local」からでも起動出来ますが、その差は良く分かりません。

「Launch」から起動します。

はい、見事動きません。

html5の互換性の問題なのか何なのか。。。

他のサンプルアプリを飛ばしてみたところ普通に動いたので、AndroidのChromeブラウザとthree.jsの問題かもしれません。

たしかにthree.jsで書いたアプリがAndroidで動いているサンプルってあまり見ないんですよね。

そもそもこのXDKのエミュレータは、ブラウザによる対応状況の差異まで考慮されていないのかもしれません。画面サイズによる差異を見る程度の話で、HTML5の互換性を100%と過程しているのだとするとあまり意味無いですね。

何にせよ、3Dで物理演算までの道のりはまだまだ長いです。。


Read More...

2013年4月21日日曜日

ブラウザで3D!Intel XDKでthree.jsを動かしてみる


では今回はIntel XDKを使って、three.jsのサンプルを動かしてみます。

この辺からthree.jsのサンプルなど含めて一式ダウンロードします。

「examples」フォルダに様々なサンプルが入っているので、Chromeで表示させながら、簡単そうなのを選んでみます。

今回選んだのは、立方体がドラッグするとクルクル回るだけの簡単なサンプル「canvas_geometry_cube.html」です。

今回、XDKの画面で新しく利用するのは、画面左側にある「Reload app」ボタンとエミュレータの右上にある「Show/Hide Debug Console」ボタンです。


前回触ったXDKのエディタ画面で、index.htmlに必要そうなところをコピペしていき、エミュレーションモードに戻ります。

「Show/Hide Debug Console」ボタンでコンソールを開き、「Reload app」ボタンで更新します。

。。。。

動きません。

コンソールを見てみましょう。


なにやら404(Not Found)が出ていますね。
それもそのはず、three.jsのライブラリをコピーするのを忘れていました。

ファイルをコピーし、パスも整えて再度リロードしてみます。


ちゃんと動きました。
マウスでドラッグするとクルクル回ります。

ちなみにリロードボタンの下に「Rotate」ボタンがあって、切り替えられます。


さらに画面右側に「DEVICE EMULATION」という欄があり、デバイスも切り替えられます。


XDKもなかなかお手軽ですが、three.jsもかなりお手軽ですね。
そのうち3Dで物理演算ぐらいまではやってみたいと思います。

Read More...

ブラウザで開発!初めてのIntel XDK


もともとappMobiが開発していたHTML5開発ツールで、Intelがスタッフごと買収して「Intel XDK」としてリリースしました。
ぱっと見た感じ便利そうだったので、軽く触ってみることにします。


まず立ち上げるとdemoアプリのHello Worldが表示されます。
新しくアプリを作成するには、左上のファイル名の左側にある「START NEW」から新規作成します。


「Project Name」を適当に決めて、今回はウィザードなどは使わずにゼロから作るので「Create your own from scratch.」を選択します。


ちなみに、Project IDは「ID.ProjectName」という規則なのですが、これID変更出来ないんでしょうか??登録に利用したメアドの@前がそのまま表示されます。
なんとなく気持ち悪いので画像は編集しつつ次へ進みます。

「Use a blank project.」を選択します。


画面に「Application Created Successfully!」と表示されて、まっさらなプロジェクトが表示されます。
以下で見えているのはエミュレーションモードで、エディタモードへ遷移するには左上にある先ほどの「START NEW」ボタンの左側「EDITOR」ボタンをクリックします。


開いた画面の左側メニューで編集したいファイルを選択すると画面右側にソースが表示されます。


生成されたindex.htmlのソースを見ると、ブランクというわりには、以下のような何やらテンプレ的なものが並んでいます。
<script type="text/javascript" charset="utf-8" src="_appMobi/appmobi_local_bootstrap.js"></script> 
<script type="text/javascript" charset="utf-8" src="http://localhost:58888/_appMobi/xhr.js"></script> 
<script type="text/javascript">
/* This function runs once the page is loaded, but appMobi is not yet active */
var init = function(){

};
window.addEventListener("load",init,false);  

/* This code prevents users from dragging the page */
var preventDefaultScroll = function(event) {
    event.preventDefault();
    window.scroll(0,0);
    return false;
};
document.addEventListener('touchmove', preventDefaultScroll, false);

/* This code is used to run as soon as appMobi activates */
var onDeviceReady=function(){
    //Size the display to 768px by 1024px
    AppMobi.display.useViewport(768,1024);
 
 //hide splash screen
 AppMobi.device.hideSplashScreen(); 
};
document.addEventListener("appMobi.device.ready",onDeviceReady,false);    
</script>

ちなみに上記を削除してリロードしてみると、以下のように怒られます。


コピペを指示されたコードは以下。
<!-- the line below is required for access to the appMobi JS library -->
<script type="text/javascript" charset="utf-8" src="http://localhost:58888/_appMobi/appmobi.js"></script>

<script type="text/javascript" language="javascript">
        // This event handler is fired once the AppMobi libraries are ready
        function onDeviceReady() {
            //use AppMobi viewport to handle device resolution differences if you want
            //AppMobi.display.useViewport(768,1024);

            //hide splash screen now that our app is ready to run
            AppMobi.device.hideSplashScreen();
        }

        //initial event handler to detect when appMobi is ready to roll
        document.addEventListener("appMobi.device.ready",onDeviceReady,false);
</script>

もともと入っていたコードと内容が違う理由は謎です。

エディタを触った感じ、「Ctrl+C」「Ctrl+Z」「Ctrl+S」などのコマンドもそのまま使えて、特に不便は感じませんでした。

ちなみに保存するとこんなポップアップが出てきます。


後は何か書いてみるだけということで、次回は実際に何か作ってみます。


今回はここまでです。

なぜ中途半端なところで終わっているかというと、調子に乗って「three.js」と「Physijs」で物理演算のサンプルを動かそうと試み、そして動かず時間だけが過ぎていき、無かったことにしたからです。。


Read More...

2013年4月14日日曜日

Chrome拡張で朝鮮半島に平和を!Browser ActionからScriptを実行する


 前回はツールバーのアイコンクリックでポップアップを表示させましたが、今回はアイコンクリックで直接スクリプトを実行します。

 拡張のアイコンをクリックすると、innerHTMLを書き換えるスクリプトを仕込みます。


 前回と違う点は「permissions」の設定と、「background」の設定です。

 「permissions」については必要なものを配列で指定します。
 今回はchrome.tabsを利用するために「tabs」を指定するのと、どのサイトでも動くように「http://*/*」(httpから始まるすべてのサイト)を指定。

 「background」については、スクリプトを登録しておくとChrome起動時に呼ばれます。
 関数を定義しておいたり、リスナーを登録しておいたりなどに利用します。

 「browser_action」の「default_popup」は、今回は使わないので削除します。

{
 "name": "南北統一",
 "version": "1.0",
 "manifest_version": 2,

 "description": "朝鮮半島を平和にします。",

 //必要なパーミッションを指定する
 "permissions": [
  "tabs", "http://*/*"
 ],

 //バックグラウンドで動く処理
 "background": {
  //スクリプトファイルを登録
  "scripts": ["background.js"]
 },

 "browser_action": {
  "default_icon": "icon.png",
  "default_title": "統一"
 }
}


 続いてスクリプトファイルを作成します。

 「chrome.browserAction.onClicked」はツールバーのアイコンがクリックされた際に呼ばれるイベントで、その「addListener」でリスナーを登録しておきます。

 「chrome.tabs.executeScript」でタブ内でスクリプトを実行します。
 第1引数はスクリプトを実行するtabIdで、現在のタブの場合はnullを指定します。
 第2引数は実行するスクリプトの詳細情報オブジェクトで、codeプロパティの場合は「:」の後ろに実行するスクリプトを文字列で指定します。

 長々と実行内容を文字列変数に格納しておいてから、executeScriptに渡していますが、もっとうまいやり方がありそうです。
 事前に関数を定義しておいてその関数を文字列で渡せば良いかと思ったのですが、やってみたら動きませんでした。

 まぁ、ここは動けば良いということで。

unification = "";
unification += 
 "document.body.innerHTML = " +
 "document.body.innerHTML.replace(/北朝鮮/g,'朝鮮');" +
 "document.body.innerHTML = " +
 "document.body.innerHTML.replace(/韓国/g,'朝鮮');";

chrome.browserAction.onClicked.addListener(function() {
 chrome.tabs.executeScript(null, {code: unification});
});

 ちなみにタグやスクリプトも無慈悲に置換されますので、「a」⇒「h1」なんてことも出来ます。


 さて、作成した拡張をChromeに登録して実行してみましょう。

 これが、

こうなる。

うーん。。

半島の統一には成功したのですが、内戦が激化しましたね。


Read More...

2013年4月13日土曜日

意外に簡単に作れる!Google Chrome ExtensionsでHello World


普通に使っているものの、書いたことの無かったGoogle Chrome Extensions(拡張機能)。ちょこっと調べてみたところ、HTMLとJavaScriptが分かれば相当お手軽に作れるということが分かりやってみました。

いくつか種類があるのですが、その中でも「Browser Action」という、ツールバーのアイコンをクリックすると動くやつが手軽そうです。

それでは、ツールバーのアイコンをクリックすると、「Hello World!」と表示するだけの簡単なものを作ってみましょう。

1.アイコンを用意する

ツールバーに表示するために19×19(px)の画像を用意します。

拡張機能一覧で表示するためのアイコンは48×48(px)の画像で、無くても動くので今回は用意しません。


2.マニフェストファイルを作成する

「manifest.json」という名前で以下のように作成します。

{
 //拡張機能の名前
 "name": "Hello World!",
 //拡張機能のバージョン
 "version": "1.0",
 //決まりごとで今は2に設定
 "manifest_version": 2,

 //拡張機能一覧に表示される説明
 "description": "Hollo Extensions!",

 "browser_action": {
  //先ほど用意したアイコン
  "default_icon": "icon.png",
  //アイコンにマウスオーバーした際に表示される文字
  "default_title": "Hello World!",
  //Hello Worldを表示するHTMLファイル
  "default_popup": "hello.html"
 }
}


3.Hello Worldを表示するHTMLを作成する

styleにmin-widthを設定しないと、変なところで改行されるので「body」に設定します。

<!DOCTYPE html>
<html lang="ja">
<head>
 <meta charaset="utf-8">
</head>
<body style="min-width:200px">
<h1>Hello World!</h1>
</body>
</html>


4.Chromeに登録する

もうあとはChromeに登録するだけです。
アイコンとマニフェストファイルとHTMLを一つのフォルダに放り込み、以下手順で登録します。

メニューから「ツール」の「拡張機能(E)」を開きます。


「デベロッパーモード」にチェックを入れ、「パッケージ化されていない拡張機能を読み込む」から該当のフォルダを選択します。


すぐ下の部分に表示されたら登録成功です。
ちなみに、拡張機能を修正したりした場合は、ここの「リロード(Ctrl+R)」をクリックすると変更が反映されます。


5.実行する

うまくいきました。

しかし、Hello Worldなんて今も使うんですかね??


Read More...

2013年3月31日日曜日

永遠のいたちごっこ!?Youtubeの動画ファイルURLを特定する

Youtubeの動画ファイルをダウンロードしたい。動画をダウンロードさせたくない。
ユーザの思いとYoutubeの中の人の思いは、いつの日も交わることのない平行線です。

ユーザ側はYoutubeサイトを解析して動画ファイルの実体のありかを見つけ出し、Youtube側はサイトの仕組みを定期的に変更しています。

2月頃に解析してファイルのURLを特定出来たのですが、久しぶりに見てみるとまた仕組みが変更になっていました。

さて、いたちごっこの始まりです。


DOMやSAXなどを使って、<script>タグの中から以下が含まれる部分を探し出します。
※ちなみに2月に見たときとはこの変数名が変わっていました。

var ytplayer = ytplayer


これはソース上に一箇所しか表示されないので、すぐ特定出来ます。
該当の場所は以下のような感じで始まります。

<script>var ytplayer = ytplayer || {};ytplayer.config = {"url": "https:


この「ytplayer.config」の中には、以下のように様々な値がJSON形式で入っています。

  • url
  • url_v9as2
  • attrs
  • params
  • url_v8
  • args
  • html5
  • assets
  • min_version
  • sts

必要なのは「args」で、またこの中に100個近い値が入っていて、その内以下二つの値を利用します。

  • url_encoded_fmt_stream_map
  • title

「url_encoded_fmt_stream_map」の中には画質の数だけカンマ区切りでURLエンコードされたデータが入っています。

url=http://r9---sn-ogueynes.c.youtube.com/videoplayback?upn=IAD3bFZV614&mv=m&source=youtube&expire=1364660261&sparams=algorithm%2Cburst%2Ccp%2Cfactor%2Cid%2Cip%2Cipbits%2Citag%2Csource%2Cupn%2Cexpire&algorithm=throttle-factor&cp=U0hVSVZRTl9LS0NONV9OS1JDOkQxNGtBcURXM0pG&id=215b73e77a669a55&sver=3&ms=au&burst=40&mt=1364634922&fexp=919112%2C910071%2C914073%2C916626%2C901449%2C932000%2C906383%2C902000%2C919512%2C929903%2C931202%2C900821%2C900823%2C931203%2C931401%2C908529%2C930807%2C919373%2C930803%2C906836%2C920201%2C929602%2C930101%2C930603%2C900824%2C910223&newshard=yes&factor=1.25&key=yt1&ip=114.185.103.241&itag=17&ipbits=8\u0026type=video/3gpp; codecs="mp4v.20.3, mp4a.40.2"\u0026sig=72F456D7BB44FD11E2FEEFF93BB7E299458D267B.B502EFA76F6358C09848C82085C4EEA0551AC8BA\u0026fallback_host=tc.v8.cache2.c.youtube.com\u0026quality=small\u0026itag=17


データ一つ一つを見ていくと、キーと値が「=」で対になった以下の値が、「\u0026」(&をアスキーコードの16進数であらわしたもの)で区切られています。

  • url
  • type
  • sig
  • fallback_host
  • quality
  • itag

「\u0026」で分割してキーと値に分けた後、「url」の値をデコードして「?」より前と後ろで分割します。

前はコレ。

http://r9---sn-ogueynes.c.youtube.com/videoplayback?


後ろには、キーと値が「=」で対になった以下の値が、「&」で区切られています。
これは全部使いますので、「&」で分割してキーと値に分けます。

  • upn
  • mv
  • source
  • expire
  • sparams
  • algorithm
  • cp
  • id
  • sver
  • ms
  • burst
  • mt
  • fexp
  • newshard
  • factor
  • key
  • ip
  • itag
  • ipbits

これに「args」に入っていた「title」を加えます。
「title」はダウンロードした際のファイル名になるので、値はそのままでも個別に指定しても良いです。

「url_encoded_fmt_stream_map」の中にあった「sig」をキーを「signature」に変更して加えます。

キーでソートします。

あとは出来上がったキーと値の組み合わせを、以下のように「url」前半部分につなげてアクセスするだけでダウンロード出来ます。

http://URL?キー=値&キー=値&キー=値&キー=値



いたちごっこは終わらない。。

Read More...