Pythonでmixiのプロフィール画像を変更する
あけましておめでとうございます。
早起きしてPython書き初めしたのでご報告です。
今年はmixiのプロフィール画像を日ごとに変更したいと思った。mixiはプロフィール用画像を複数設定できて、その中から一枚をメインの画像にする。APIとか分からないので mechanize でやれば良いやって思ったのだけど、やってみたら少しハマったのでメモ。
とりあえず成果物
https://gist.github.com/1545546
使い方
- メールアドレスとパスワードを使ってログイン。
- プロフィール画像の一覧から現在のプロフィール画像を削除(しなくても良い)
- 新しいプロフィール画像を追加(level="1"は友人向け、"2"は全体向け)
- 画像はjpegのみ(mixiの仕様)
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 ChromeのJavaScriptデバッガの進化がすごい - 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);
今回は外部ファイルを使わない無敵タイマー。以下手順
- BlobBuilderっていうのを使ってコードを組み立てる。
- すごいと評判*1 の createObjectURL でURLを取得
- 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
どちらも音のなるウェブ楽器で、別タブを選択中も音が鳴り続ける。
(以前は別タブを選択すると音が激しく途切れていた)
メモ
コード
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; } };
実際のコードとはちょっと違うけど大体こんな感じ
- PLAY_INTERVALの間隔(50msくらい)でタイマー処理をしている
- self._stream は再生する音のシグナル配列で self._audio.mozWriteAudio(self._stream) で再生
- mozWriteAudioは非ブロックなので次の音のシグナル配列を読み込む
- 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
何かを自動生成するようなプログラムを書くときマルコフ連鎖を使いたくなることは多いと思う。で、それ自体はそんなに難しくないんだけど、大体微妙な感じに仕上がってしまうので、なかったことにしてしまうことが多い。でも今回は諦めずに調整して比較的うまくいった。
工夫したところ
お知らせ
JavaScriptでリアルタイムに音を出す簡単便利なやつ、CoffeeScriptで無駄が多いので JavaScript だけで書き直した最新版を gist に pico-player.js として置いています。昨日の今日でごめんなさい。
https://gist.github.com/1342081