はじめに
Twitter アイコンが音楽になったら良いなー、と思って作ってみました。
仕組み
コードはかなーり適当ですが、ざっと以下のような感じです。
- API 経由で twitter アイコン画像の URL を取得
- http.get でその画像を Buffer に格納
- node-canvas でサーバ側の Canvas に Buffer を描画
- Canvas から取得したピクセルデータをもとに jsmidgen を利用して MIDI を生成
で、画像を音楽にするアルゴリズムですが、以下のような感じで適当にやっています。
- アイコンの左上から縦に 4 px 刻みでピクセル情報を取得
- 楽器は画像の中央ピクセルの色相から適当に決定
- ドレミは RGB 情報から適当に生成
- テンポは彩度で決定(彩度が高いほうがゆっくり)
- 音の高さは明度で決定(明るいほうが高い)
という感じです。単色アイコンの方すみません。
コードは以下のようになります。
var Canvas = require('canvas') , Image = Canvas.Image , Midi = require('jsmidgen') , http = require('http') , fs = require('fs') , exec = require('child_process').exec , twitter = require('twitter') , async = require('async') ; // ユーザー情報取得用 API var user_lookup_api_url = 'https://api.twitter.com/1.1/users/lookup.json'; // Twitter へ接続 var twit = new twitter({ consumer_key : 'xxxxxxxxxx', consumer_secret : 'xxxxxxxxxx', access_token_key : 'xxxxxxxxxx', access_token_secret : 'xxxxxxxxxx' }); // 音階 var notes = ['c','d','e','f','g','a','b']; // 楽器 var instruments = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F]; // MIDI データ function getNote(r, g, b) { // 正規化 r /= 255; g /= 255; b /= 255; // RGB -> HSV 変換 var max = Math.max(r, g, b); var min = Math.min(r, g, b); var h = (max === min) ? 0 : (max === r) ? 60 * (g - b)/(max - min) + 0 : (max === g) ? 60 * (b - r)/(max - min) + 120 : 60 * (r - g)/(max - min) + 240; if (h < 0) h += 360; var s = (max === 0) ? 0 : (max - min) / max; var v = max; // HSV を MIDI に変換 var scale = Math.floor(r*g*b*7)%notes.length; var length = Math.floor(s * 96) + 12; var level = Math.floor(v * 3) + 3; var symbol = notes[scale] + level; var instrument = instruments[Math.floor(h)%instruments.length]; return { scale : scale, level : level, length : length, symbol : symbol, instrument : instrument }; } // サーバを立てる http.createServer(function(req, res) { var id = req.url.slice(1); if (id === 'favicon.ico') return; // アイコンを音楽に変換する async.waterfall([ // Tiwtter からアイコンのURLを取得 function (callback) { twit.get(user_lookup_api_url, {screen_name: id}, function(user_info) { console.log(user_info); if ('statusCode' in user_info && user_info.statusCode == 404) { callback('Error!: @' + id + ' is not found'); return; } callback(null, user_info[0].profile_image_url); }); }, // アイコン画像を取得 function (url, callback) { http.get(url, function(res) { // Buffer で読み込まないと img.src が上手くいかない var data = new Buffer(parseInt(res.headers['content-length'], 10)) var pos = 0; res.on('data', function(chunk) { chunk.copy(data, pos); pos += chunk.length; }).on('end', function() { // ストリームから画像を用意 var img = new Image img.src = data; // ピクセルデータを得るためのキャンバスを用意 var canvas = new Canvas(img.width, img.height) var ctx = canvas.getContext('2d') // 描画してピクセルデータを得る ctx.drawImage(img, 0, 0, img.width, img.height); var imgData = ctx.getImageData(0, 0, img.width, img.height); imgData['width'] = img.width; imgData['height'] = img.height; callback(null, imgData); }); }).on('error', function(e) { callback(e, null); }); }, // 画像から MIDI を生成 function (img, callback) { var d = img.data; var RGBA_NUM = 4; var SKIP_PX = 4; var file = new Midi.File(); var track = new Midi.Track(); file.addTrack(track); // 適当な楽器を当てはめる var m = img.width*img.height * 4 / 2; var note = getNote(d[m], d[m+1], d[m+2], d[m+3]); track.setInstrument(0, note.instrument); // 適当な音を当てはめる for (var i = 0; i < img.width; i += SKIP_PX) { for (var j = 0; j < img.height; j += SKIP_PX) { var k = i*RGBA_NUM + j*RGBA_NUM*img.width; var note = getNote(d[k], d[k+1], d[k+2], d[k+3]); track.addNote(0, note.symbol, note.length); } } callback(null, file); }, // MIDI を書き出し function (file, callback) { res.writeHead(200,{'Content-Type': 'audio/midi'}); res.write(file.toBytes(), 'binary'); res.end(); }, ], function(err) { res.writeHead(200,{'Content-Type': 'text/plain'}); res.write(err); res.end(); }); }).listen(50000); // 例外処理 process.on('uncaughtException', function (err) { console.log('uncaughtException => ' + err); });