Pythonでmixiのプロフィール画像を変更する

あけましておめでとうございます。
早起きしてPython書き初めしたのでご報告です。


今年はmixiプロフィール画像を日ごとに変更したいと思った。mixiはプロフィール用画像を複数設定できて、その中から一枚をメインの画像にする。APIとか分からないので mechanize でやれば良いやって思ったのだけど、やってみたら少しハマったのでメモ。

とりあえず成果物

https://gist.github.com/1545546


使い方

    print "login"
    m = MixiProfileImage( EMAIL_ADDRESS, PASSWORD )
    
    print "delete current profile image"
    images = m.filter(level="1")
    for k, v in images.iteritems():
        m.delete(v)

    print "add new profile image"
    image = m.add( FILE_PATH , level="1", is_main="y")

他には 公開範囲、メイン画像どうかの 変更 と 絞り込み ができる。
mechanize なので mixi の仕様が変わったら使えなくなる。 

日替わりプロフィールに使える画像たち(参考)

http://www.mcdonalds.co.jp/menu/regular/index.html
http://www.akindo-sushiro.co.jp/menu/item.php
http://www.abysse.co.jp/world/flag/index.html
http://hanshintigers.jp/data/player/2012/index.html#pitcher

同じカテゴリーの画像だと日替わりアイコンにしても混乱させないはず。

仕組み

mechanizeで操作している。


画像の追加はファイルをPOSTで送信するだけなので簡単。

# self.client = mechanize.Browser()
self.client.select_form(nr=0)
self.client["level"] = [level]
    
self.client.add_file(open(path), "image/jpeg", path)
self.client.set_all_readonly(False)
self.client.submit()


画像の削除でハマった。
実際にブラウザで操作すると分かるのだけど、マウスでの操作が何段階かある。それらはJavaScriptで制御されていて最終的にajaxでリクエストを投げている(動きから判断した)。なので、mechanizeでその操作をそっくりなぞってみようとしたら、mechanizeはJavaScriptの動作は再現されない。そこで最終的に投げられるajaxのリクエストを調べて真似た。


以下やったこと

  • chrome developer tools を開く(Control+Alt+i)

使い方はこの記事が参考になる

Google ChromeJavaScriptデバッガの進化がすごい - 0xFF
http://d.hatena.ne.jp/os0x/20110422/1303468821

  • Scriptsタグの XHR Breakpoints の Any XHR のチェックを ON
  • 画像削除の一連の動作を普通にマウスでする
  • protorype-effects-なんとかファイルの途中でブレーク
  • ソースの下部にある{}をクリックすると、933行目で止まっている
this.transport.open(this.method.toUpperCase(), this.url, this.options.asynchronous);
if (this.options.asynchronous) {
    this.respondToReadyState.bind(this).defer(1)
}
this.transport.onreadystatechange = this.onStateChange.bind(this);
this.setRequestHeaders();
this.body = this.method == "post" ? (this.options.postBody || D) : null;
this.transport.send(this.body); // <- break
  • 上記ソース1行目の this.url に 最終行 this.body を POST で送信しているのが分かる
  • this.url には secret っていうキーみたいなのがある
  • とりあえず普通にHTMLソースを調べると secret っぽいキーがあったのでそれを利用する
RPC_URL = "/system/rpc.json?auth_type=postkey&secret=%s" % secret
params = '{"jsonrpc": "2.0", "method": "jp.mixi.profileimage.deleteEntry", "params": {"image_name": "%s"}, "id": 0}' % image_name

self.client.open(url, params)
  • できた!!!

JavaScriptの継承できるクラスの書き方

インスタンス変数を参照したいときは普通に this 、自クラスや親クラスのメソッドを呼び出したいときは $this を使う。
特殊なことはしていないんだけど、変数名を工夫したら何か分かりやすくなった。

// 継承関数
var extend = function(Klass, SuperKlass) {
    var F = function() {};
    F.prototype = SuperKlass.prototype;
    Klass.prototype = new F();
    Klass.prototype.superclass = SuperKlass.prototype;
    return Klass.prototype;
};


var ClassA = (function() {
    var ClassA = function() {
        this.initialize.apply(this, arguments);
    }, $this = extend(ClassA, Object);

    $this.initialize = function() {
        this.value = "A";
    };
    $this.hoge = function() {
        console.log("ClassA.hoge!");
        $this.fuga.call(this); // fuga.call(this); でもOK
    };
    var fuga = $this.fuga = function() {
        console.log("ClassA.fuga! this.value=" + this.value);
    };
    return ClassA;
}());

var ClassB = (function() {
    var ClassB = function() {
        this.initialize.apply(this, arguments);
    }, $this = extend(ClassB, ClassA);
    
    $this.initialize = function() {
        this.value = "B";
    };
    
    $this.hoge = function() {
        console.log("ClassB.hoge!");
        $this.superclass.hoge.call(this);
    };
    
    $this.fuga = function() {
        console.log("ClassB.fuga! this.value=" + this.value);
        $this.superclass.fuga.call(this);
    };
    
    return ClassB;
}());


var ClassC = (function() {
    var ClassC = function() {
        this.initialize.apply(this, arguments);
    }, $this = extend(ClassC, ClassB);

    $this.initialize = function() {
        $this.superclass.initialize(this, arguments);
        this.value = "C";
    };

    $this.hoge = function() {
        console.log("ClassC.hoge!");
        $this.superclass.hoge.call(this);
    };

    return ClassC;
}());


var a = new ClassA();
var b = new ClassB();
var c = new ClassC();


c.hoge(); // ClassC.hoge -> ClassB.hoge -> ClassA.hoge -> ClassA.fuga と呼ばれる
console.log("----------------------------------------");
c.fuga(); // ClassB.fuga -> ClassA.fuga と呼ばれる


console.log("----------------------------------------");
console.log("* overwrite ClassB.fuga");
b.__proto__.fuga = function() {
    console.log("piyopiyo");
};
c.fuga(); // 上書きされた ClassB.fuga が呼ばれる

JavaScriptでテンプレート文字列を作る

今つくっているプログラムで必要そうだったので書いた。

やりたいこと

"$0...$1の年収、$2..?"

みたいなテンプレートがあって、それに $0=うわっ, $1=私, $2=低すぎ を渡すと、「うわっ...私の年収、低すぎ..?」と変換したい。

案1: string.replace

template = "$0...$1の年収、$2..?";
$0 = "うわっ";
$1 = "私";
$2 = "低すぎ";

// incorrect
result = template.replace("$0", $0); 

// correct
result = template.replace(/\$0/g, $0); 
result = result.replace(/\$1/g, $1);
result = result.replace(/\$2/g, $2);

簡単にできる。JavaScript の replace は何故か1つしか置換してくれないので、最初の行のやり方は良くない。正規表現にグローバルオプションをつけて置換してあげれば良い。ただ、replaceする度に対象箇所を探しているので非効率な感じがする。テンプレートっていうのは置換したい場所は把握しているもんだ。毎回探すとかアホか。あと、言ってなかったけどバックスラッシュに続く $ はエスケープしたい。しかし、JavaScript正規表現 は何故か後読みしてくれないので、後の行のやり方も良くない。


案2: toString + join

JavaScript のオブジェクトには toString っていうメソッドがあって、文字列に変換したいときはこれが呼ばれる。そして配列には join っていうメソッドがあって、配列の中身を「文字列に変換して」くっつける。

つまり、こうできる。

$0 = {value:"ええっ"       , toString:function() {return this.value;}};
$1 = {value:"お前"         , toString:function() {return this.value;}};
$2 = {value:"そんなにあるの", toString:function() {return this.value;}};

template = [$0, "...", $1, "の年収、", $2, "..?"];

result = template.join("");

こっちのほうが早い。簡単に速度を計測したものを最後に載せておく。

コード

テンプレート文字列のクラスを作った。$0-$9まで使えて、エスケープもできる。

function TemplateString() {
    var str, defaultValue;
    
    if (arguments.length === 1) {
        str = arguments[0];
        defaultValue = [];
    } else if (arguments.length >= 2) {
        str = arguments[0];
        defaultValue = arguments[1];
    }
    
    this.vars = (function(n, defaultValue) {
        var list = [];
        var i, imax;
        for (i = 0; i < n; i++) {
            list[i] = {value:defaultValue[i]||"",
                       toString:function(){return this.value;}};
        }
        return list;
    }(10, defaultValue));
    
    this.set(str);
}

TemplateString.prototype.toString = function() {
    if (this.withT) {
        return this.value.join("");
    } else {
        return this.value;
    }
};

TemplateString.prototype.setvalue = function(index, value) {
    if (0 <= index && index < this.vars.length) {
        this.vars[index].value = value;
    }
};

TemplateString.prototype.set = function(str) {
    if (/[^\\]\$[0-9]/.test(" " + str)) {
        this.withT = true;
        this.value = this._make(str);
    } else {
        this.withT = false;
        this.value = str;
    }
};

TemplateString.prototype._make = function(str) {
    var list;
    var c, esc, dol, buffer;
    var i, imax;
    list = [];
    buffer = [];
    for (i = 0, imax = str.length; i < imax; i++) {
        c = str[i];
        if (c === "$" && !esc) {
            dol = true;
        } else if (c === "\\") {
            esc = true;
        } else {
            if (dol && "0" <= c && c <= "9") {
                if (buffer.length > 0) {
                    list.push(buffer.join(""));
                    buffer = [];
                }
                list.push(this.vars[c|0]);
            } else {
                buffer.push(c);
            }
            esc = false;
            dol = false;
        }
    }
    if (buffer.length > 0) list.push(buffer.join(""));
    return list;
};

速度比較

短い文字列 {うわっ}...{私}の年収、{低すぎ}..?

50000回 2500000回
replace 110msec 4330msec
toSting+join 40msec 2330msec
TemplateString 50msec 2540msec

長い文字列 走れメロスの冒頭677文字

500000回
replace 4550msec
TemplateString 1740msec
var limit = 50000;
(function(limit) {
    var template, lis, $0, $1, $2;
    var d, i, result;
    lis = ["うわっ", "私", "低すぎ"];
    d = +new Date();
    template = "$0...$1の年収、$2..?";
    for (i = 0; i < limit; i++) {
        $0 = lis[(i+0)%3];
        $1 = lis[(i+1)%3];
        $2 = lis[(i+2)%3];
        result = template.replace(/\$0/g, $0);
        result = result.replace(/\$1/g, $1);
        result = result.replace(/\$2/g, $2);
    }
    console.log("TIME", (+new Date() - d), result);
}(limit));

(function(limit) {
    var template, lis, $0, $1, $2;
    var d, i, result;
    lis = ["うわっ", "私", "低すぎ"];
    d = +new Date();
    $0 = {value:"", toString:function() {return this.value;}};
    $1 = {value:"", toString:function() {return this.value;}};
    $2 = {value:"", toString:function() {return this.value;}};
    template = [$0, "...", $1, "の年収、", $2, "..?"];
    for (i = 0; i < limit; i++) {
        $0.value = lis[(i+0)%3];
        $1.value = lis[(i+1)%3];
        $2.value = lis[(i+2)%3];
        result = template.join("");
    }
    console.log("TIME", (+new Date() - d), result);
}(limit));

(function(limit) {
    var template = "$0...$1の年収、$2..?";
    var lis;
    var d, i, tstr ,result;
    lis = ["うわっ", "私", "低すぎ"];
    d = +new Date();
    tstr = new TemplateString(template);
    for (i = 0; i < limit; i++) {
        tstr.setvalue(0, lis[(i+0)%3]);
        tstr.setvalue(1, lis[(i+1)%3]);
        tstr.setvalue(2, lis[(i+2)%3]);
        result = tstr.toString();
    }
    console.log("TIME", (+new Date() - d), result);
}(limit));

console.log("----------------------------------------");

(function() {
    var limit = 500000;
    var template = "$0は激怒した。必ず、かの邪智暴虐の王を除かなければならぬと決意した。$0には政治がわからぬ。$0は、村の牧人である。笛を吹き、羊と遊んで暮して来た。けれども邪悪に対しては、人一倍に敏感であった。きょう未明$0は村を出発し、野を越え山越え、十里はなれた此のシラクスの市にやって来た。$0には父も、母も無い。女房も無い。十六の、内気な$2と二人暮しだ。この$2は、村の或る律気な一牧人を、近々、花婿として迎える事になっていた。結婚式も間近かなのである。$0は、それゆえ、花嫁の衣裳やら祝宴の御馳走やらを買いに、はるばる市にやって来たのだ。先ず、その品々を買い集め、それから都の大路をぶらぶら歩いた。$0には竹馬の友があった。$1である。今は此のシラクスの市で、石工をしている。その友を、これから訪ねてみるつもりなのだ。久しく逢わなかったのだから、訪ねて行くのが楽しみである。歩いているうちに$0は、まちの様子を怪しく思った。ひっそりしている。もう既に日も落ちて、まちの暗いのは当りまえだが、けれども、なんだか、夜のせいばかりでは無く、市全体が、やけに寂しい。のんきな$0も、だんだん不安になって来た。路で逢った若い衆をつかまえて、何かあったのか、二年まえに此の市に来たときは、夜でも皆が歌をうたって、まちは賑やかであった筈だが、と質問した。若い衆は、首を振って答えなかった。しばらく歩いて老爺に逢い、こんどはもっと、語勢を強くして質問した。老爺は答えなかった。$0は両手で老爺のからだをゆすぶって質問を重ねた。老爺は、あたりをはばかる低声で、わずか答えた。";
    var $0 = "メロス";
    var $1 = "セリヌンティウス";
    var $2 = "妹";

    (function(limit, template, lis) {
        var d, i, result;
        d = +new Date();
        for (i = 0; i < limit; i++) {
            result = template.replace(/\$0/g, lis[(i+0)%3]);
            result = result.replace(/\$1/g, lis[(i+1)%3]);
            result = result.replace(/\$2/g, lis[(i+2)%3]);
        }
        console.log("TIME", (+new Date() - d), result.substr(0, 20));
    }(limit, template, [$0, $1, $2]));

    (function(limit, template, lis) {
        var d, i, tstr ,result;
        d = +new Date();
        tstr = new TemplateString(template);
        for (i = 0; i < limit; i++) {
            tstr.setvalue(0, lis[(i+0)%3]);
            tstr.setvalue(1, lis[(i+1)%3]);
            tstr.setvalue(2, lis[(i+2)%3]);
            result = tstr.toString();
        }
        console.log("TIME", (+new Date() - d), result.substr(0, 20));
    }(limit, template, [$0, $1, $2]));
}());

BlobBuilder で 外部ファイルの要らない WebWorkers はつくれる

WebWorkersで別タブ選択中もきっちり動く無敵タイマーをつくる - つまみ食う
http://d.hatena.ne.jp/mohayonao/20111108/1320756534


先日書いたこのエントリーで、別タブを選択したときも精度が落ちないタイマーを作った。
でも、外部ファイルが必要だったのでライブラリ的なものに組み込むときにパスを自己解決できないという問題があった。

// 問題点:muteki-timer.js の設置場所が制限される
//        もしくはライブラリの外から muteki-timer.js の場所を教えてあげる必要がある
var url = "muteki-timer.js";
var timer = new Worker(url); 


今回は外部ファイルを使わない無敵タイマー。以下手順

  1. BlobBuilderっていうのを使ってコードを組み立てる。
  2. すごいと評判*1 の createObjectURL でURLを取得
  3. new Worker(url) する


ちなみに前回の方法は

  1. 外部ファイルを書く (muteki-timer.js)
  2. url = "./muteki-timer.js"
  3. new Worker(url) する

MutekiTimerクラス

上記の方法で無敵タイマークラスを作った。
無敵タイマーが使えないときは通常の setInterval を使う。コードはエントリの最後。

デモ

無敵タイマー と 通常の setInterval の比較ページ。
別タブを選択して、しばらく放置して戻ってくると差が分かる。
http://mohayonao.herokuapp.com/mutekitimer

サンプル

無限インベンション
http://mohayonao.herokuapp.com/invention


6文字リズムマシン
http://mohayonao.hatenablog.com/entry/2011/11/21/202019


どちらも音のなるウェブ楽器で、別タブを選択中も音が鳴り続ける。
(以前は別タブを選択すると音が激しく途切れていた)

メモ

  • Chrome, Firefox で使える
  • Chrome では ファイル(htmlをダブルクリックして開いた状態)だと動かない
    • URL.createObjectURL が undefined を返す
  • Firefox は ローカル(127.0.0.1 or localhost)だと動かない??
    • ちょっと前にそれで困った覚えがあるけど、今はそうでもない。バージョン依存?
  • たしかに createObjectURL はすごい。よく分からないけどもっと凄いことに使えそう。

コード

    function MutekiTimer() {
        this.initialize.apply(this, arguments);
    }
    MutekiTimer.prototype = {
        initialize: function() {
            var url = (function() {
                var BlobBuilder = window.WebKitBlobBuilder || window.MozBlobBuilder;
                var URL = window.URL || window.webkitURL;
                var MutekiTimerBlob;
                if (!BlobBuilder || !URL) return null;
                
                MutekiTimerBlob = new BlobBuilder();
                MutekiTimerBlob.append("var timerId = 0;");
                MutekiTimerBlob.append("this.onmessage = function(e) {");
                MutekiTimerBlob.append("  if (timerId !== 0) {");
                MutekiTimerBlob.append("    clearInterval(timerId);");
                MutekiTimerBlob.append("    timerId = 0;");
                MutekiTimerBlob.append("  }");
                MutekiTimerBlob.append("  if (e.data > 0) {");
                MutekiTimerBlob.append("    timerId = setInterval(function() {");
                MutekiTimerBlob.append("    postMessage(null);");
                MutekiTimerBlob.append("    }, e.data);");
                MutekiTimerBlob.append("  }");
                MutekiTimerBlob.append("};");
                return URL.createObjectURL(MutekiTimerBlob.getBlob());
            }());
            if (url) {
                this._timer = new Worker(url);
                this.isMuteki = true;
            } else {
                this._timer = null;
                this.isMuteki = false;
            }
            this._timerId = 0;
        },
        setInterval: function(func, interval) {
            if (this._timer) {
                this._timer.onmessage = function(e) {
                    func();
                };
                this._timer.postMessage(interval);
            } else {
                if (this._timerId !== 0) {
                    clearInterval(this._timerId);
                }
                this._timerId = setInterval(function() {
                    func();
                }, interval);
            }
        },
        clearInterval: function() {
            if (this._timer) {
                this._timer.postMessage(0);
            } else {
                if (this._timerId !== 0) {
                    clearInterval(this._timerId);
                }
                this._timerId = 0;
            }
        }
    };

Instagramがエンジニアを見つけるために出題した独創的な課題をやってみた

面白そうなのでやってみた。


あなたは解ける? Instagramがエンジニアを見つけるために出題した独創的な課題:Don't be lame
http://kenichinishimura.blogspot.com/2011/11/instagram.html


縦に分割された画像をゴチョゴチョやって元に戻す。


これを


こうする


以下、コードの中にコメントで書いた。
ぜんぜんエレガントじゃない。。


2011/11/16 追記
ぜんぜんエレガントじゃないどころか、解けていなかった。。
最後に画像のつながりを推測する段階で、いちばんつながっていない箇所が画像の端っこになるはずだけど、下のコードでは最後から2番目を端っこにしている(ズルしている)


2011/11/16 さらに追記
色をRGBからHSVに変換して、閾値をいじったら解けるようになった。
ただし、他の画像でも同じように出来るかは分からない。たぶん良い閾値を発見できればいけると思うけど。


#!/usr/bin/env python
# -*- coding: utf-8 -*-

import time
import math
import Image


def RGBtoHSV(r, g, b):
    """RGBをHSVに変換"""
    maxValue = max(r, max(g, b))
    minValue = min(r, min(g, b))
                   
    if maxValue == minValue:
        h = 0;
    elif maxValue == r:
        h = (60 * (g - b) / (maxValue - minValue) + 360) % 360;
    elif maxValue == g:
        h = (60 * (b - r) / (maxValue - minValue)) + 120;
    elif maxValue == b:
        h = (60 * (r - g) / (maxValue - minValue)) + 240;   
        
    if maxValue == 0:
        s = 0;
    else:
        s = (255 * ((maxValue - minValue) / maxValue));
        
    v = maxValue;

    return h, s, v;
	


def calcLinesDistance(d1, d2):
    """(行単位で)色の距離を求める"""
    calcDistance = lambda c1,c2: math.sqrt((c1[0]-c2[0])**2 + (c1[1]-c2[1])**2 + (c1[2]-c2[2])**2)
    return sum(calcDistance(d1[x], d2[x]) for x in xrange(len(d1))) / len(d1)


def guessSplitHeight(im, nlist=(2,4,6), thlist=(1.5,1.7)):
    """何ピクセルで分割されているか推測する"""
    
    width, height = im.size
    
    # 画像を行ごとのHSV値に変換
    data = tuple(RGBtoHSV(*c[:3]) for c in im.getdata())
    data = tuple(data[i*width:(i+1)*width] for i in xrange(height))
    
    # 各行ごとに次の行との差(距離)を計算する
    lis = tuple(calcLinesDistance(data[y-1], data[y]) for y in xrange(1, height))

    candidates = {}
    for n in nlist: # n は移動平均の数
        for th in thlist: # th は閾値
            prev = -1
            for i in xrange(n, len(lis)-n):
                # 周りに比べて距離が大きい??
                x = lis[i] / (sum(lis[i-n:n+i+1]) / (2 * n + 1))
                if x > th:
                    # 閾値を超えたら、前回の閾値を超えたときとの間隔をカウント
                    key = (i-prev)
                    if key not in candidates:
                        candidates[key] = 0
                    candidates[key] += 1
                    prev = i

    # いちばん良く出てくる間隔があやしい
    return sorted(candidates.items(), key=lambda a:-a[1])[0][0]



def guessConnect(im, n):
    """画像のつながりっぷりを推測する"""

    width, height = im.size
    h = height/n
    
    # 画像を分割サイズで切り抜く、
    # ついでに半分のサイズに(計算量を減らすのとある程度平均値で調査するため)
    cropper = lambda i: im.crop((0, i * h, width, (i+1)*h)).resize((width/2, h/2), Image.CUBIC)

    # 画像の端っこのデータだけ取る
    lis = []
    for i in xrange(n):
        data = tuple(RGBtoHSV(*c[:3]) for c in cropper(i).getdata())
        data = tuple(data[i*width/2:(i+1)*width/2] for i in xrange(h/2))
        lis.append(dict(L=data[0], R=data[-1]))
        
    # 分割したそれぞれの距離を求める (距離が小さい=自然にくっつきそう)
    distanceList = {}
    for i in xrange(n):
        for j in xrange(n):
            if i == j: continue
            d = calcLinesDistance(lis[i]["R"], lis[j]["L"])
            distanceList[(i, j)] = d

    # 距離が小さい順番につながりを確定させていく
    right, left = range(n), range(n)
    lis = []
    while len(left) > 0:
        mind = float("inf")
        pair = None
        for i in xrange(n):
            if i not in left: continue
            for j in xrange(n):
                if i == j: continue
                if j not in right: continue
                d = distanceList[(i, j)]
                if d < mind:
                    pair = (i, j, d)
                    mind = d
        left.remove(pair[0])
        right.remove(pair[1])
        lis.append(pair)
        
    # 順番にならべる
    map = dict(x[:2] for x in lis)
    head = key = lis[-1][1]
    result = [head]
    for i in xrange(n):
        result.append(map[key])
        key = map[key]
    return result


def main():
    begin = time.time()
    
    imgpath = "./instagram_shuttered.png"

    im1 = Image.open(imgpath)
    width, height = im1.size
    
    # 縦に捜査するの気持ち悪いので回転させる
    im2 = im1.rotate(-90)

    # 分割の間隔を推測する
    h = guessSplitHeight(im2.resize((height/4, width)))
    
    # つながりを推測する
    l = guessConnect(im2, width/h)

    # 張り合わせる
    im2 = Image.new(im1.mode, im1.size)
    for i1, i2 in enumerate(l):
        cpi = im1.crop((i2 * h, 0, (i2+1) * h, height))
        im2.paste(cpi, (i1 * h, 0, (i1+1) * h, height))
    im2.save("./result.png")
    
    print time.time() - begin, "sec"

if __name__ == "__main__":
    main()

WebWorkersで別タブ選択中もきっちり動く無敵タイマーをつくる

ずっと悩んでいたのだけど、僕の書いた簡単便利プレイヤーではブラウザの別タブを選択すると音が途切れまくって格好よくなるという問題があった。原因はわかっていて setInterval でタイマー処理していると、別タブ選択時に精度が非常に悪くなる。requestAnimationFrame と同じで見ていないからサボるってことなんだけど、音を出してるときはサボられると困るわけで、もっとこう、無敵なタイマーがないものかと思っていた。

そしたら id:ultraist さんにコメントをもらって、WebWorkers を使えばどうにかなるっぽい事がわかった。

WebWorkersはバックグラウンド処理するためのAPIでメッセージのやり取りで並列処理ができる。


Web Workers
http://www.whatwg.org/specs/web-apps/current-work/multipage/workers.html
http://www.html5rocks.com/en/tutorials/workers/basics/
http://dev.w3.org/html5/workers/
http://ascii.jp/elem/000/000/560/560326/


まだ、ちゃんと読んでいないんだけど、ちょっと修正しただけで簡単に無敵タイマーが作れたので書いておく。

問題のコード

MozPlayer.prototype.play = function() {
    var self = this;
    this._timerId = setInterval(function() {
        self._audio.mozWriteAudio(self._stream);
        self._stream = self._generator.next();
    }, this.PLAY_INTERVAL);
};
MozPlayer.prototype.stop = function() {
    if (this._timerId !== 0) {
        clearInterval(this._timerId);
        this._timerId = 0;
    }
};

実際のコードとはちょっと違うけど大体こんな感じ

  1. PLAY_INTERVALの間隔(50msくらい)でタイマー処理をしている
  2. self._stream は再生する音のシグナル配列で self._audio.mozWriteAudio(self._stream) で再生
  3. mozWriteAudioは非ブロックなので次の音のシグナル配列を読み込む
  4. 2に戻る

ここで setInterval を使っているのがよくない。別タブを選択するとサボりだして、音が途切れる。

WebWorkers を使った無敵タイマーバージョン

まず別ファイルが必要

// muteki-timer.js
var timerId = 0;
onmessage = function(e) {
    if (timerId !== 0) {
        clearInterval(timerId);
        timerId = 0;
    }
    if (e.data > 0) {
        timerId = setInterval(function() {
            postMessage(null);
        }, e.data);
    }
};

メッセージに応じて空メッセージを送るだけのタイマーを起動/終了する。


プレイヤーのコード修正

MozPlayer.prototype.init = function() {
    var self = this;
    this._timer = new Worker("muteki-timer.js");
    this._timer.onmessage = function(e) {
        self._audio.mozWriteAudio(self._stream);
        self._stream = self._generator.next();
    };
};
MozPlayer.prototype.play = function() {
    this._timer.postMessage(this.PLAY_INTERVAL);
};
MozPlayer.prototype.stop = function() {
    this._timer.postMessage(0);
};

大体こんな感じ。コードの場所が変わっただけで中身はそのまま。超簡単。
メッセージのやり取りの分だけオーバーヘッドがあるんだけど、別タブ選択時に音が途切れなくなるメリットの方が大きい。オーバーヘッドって言っても些細なものみたいだし。


非同期パフォーマンス - JavaScriptで遊ぶよ - g:javascript
http://javascript.g.hatena.ne.jp/edvakf/20100227/1267246371


ただし、ひとつハマった部分があって、僕の環境(OSX Firefox 8, 127.0.0.1:3000)で new Worker() をコールすると "Could not get domain!" ってエラーが出て Worker のインスタンスが作れない。デプロイすると動く。


これっぽい
https://bugzilla.mozilla.org/show_bug.cgi?id=683280



今回はトリガーとしてしか使っていないけど、UIのスレッドとは別の場所(スレッド)で実行されるので、音楽系の処理は全部バックグランドにまわすとか、ガンガン使うとガンガン早くなりそう。

メモ

  • メッセージは 数値、文字列、リスト、オブジェクト(要はJSON)が送れる
  • new Worker() の引数がファイル名なのちょっとつらい
  • 非常に時間がかかる処理を泣く泣くsetIntervalで分割処理したりしなくてすむ(むしろこれが本来の使い方)
  • BlobBuilderを使えば別ファイルでなくてもよいみたい

ウェブ楽器

別タブを選択しても音が途切れない優れもの!無料です!!
Chrome(一番よい), Firefox(まあまあ), Opera(いまいち) に対応しています。


Endless Invention (バッハインターフェイスの無限インベンション)
http://mohayonao.herokuapp.com/invention


windmills (大量の風車インターフェイスのやつ)
http://mohayonao.herokuapp.com/windmills


KSDN-808II (関西電気保安協会リズムマシーン)
http://ksdn808.herokuapp.com/


ONE-LINER-ORCHESTRA (短いコードで音楽つくるやつ)
http://one-liner-orchestra.herokuapp.com/

永久にバッハのインベンションを演奏するやつをつくった

Endless Invention
http://mohayonao.herokuapp.com/invention


何かを自動生成するようなプログラムを書くときマルコフ連鎖を使いたくなることは多いと思う。で、それ自体はそんなに難しくないんだけど、大体微妙な感じに仕上がってしまうので、なかったことにしてしまうことが多い。でも今回は諦めずに調整して比較的うまくいった。

工夫したところ

  • 八分音符は十六分音符ふたつといった具合に音の長さをいったん揃えている
  • ラが鳴っているときはミが鳴っていることが多いみたいな、二声がどう重なっているかも調べた。先の音長の正規化を行っているので簡単だった。
  • 正規化を行っているため、頻繁に同じ音の繰り返しが発生する。同じ音が続いたら無視する?ベロシティ下げてMIDIディレイっぽくする?今回は後者
  • ファミコンっぽい音
  • 半分現実で半分夢みたいな雰囲気を出すためにエフェクトをつけた
  • 悪夢っぽいインターフェイス

お知らせ

JavaScriptでリアルタイムに音を出す簡単便利なやつ、CoffeeScriptで無駄が多いので JavaScript だけで書き直した最新版を gist に pico-player.js として置いています。昨日の今日でごめんなさい。
https://gist.github.com/1342081