Created
July 12, 2012 00:26
-
-
Save mohayonao/3094681 to your computer and use it in GitHub Desktop.
WebBeeper 2A03 にコメントをつけたやつ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* WebBeeper 2A03 | |
* Origin : http://www.g200kg.com/en/docs/webbeeper | |
* Author : g200kg氏 | |
* Comment: mohayonao | |
* Blog : http://mohayonao.hatenablog.com/entry/2012/07/12/093004 | |
*/ | |
var app; | |
var audioif; | |
var canvas; | |
var imgkeys,imgval,imgval2,imgch,imgform,imgpenv,imgenv; | |
var samplerate; | |
var rsamplerate; | |
var outbuf; | |
var outbufsize; | |
// プログラム設定(音色の設定テーブル) | |
var prgtable = [ | |
// 順番に | |
// Form : 波形の種類 | |
// Pitch: ピッチ変化の種類 | |
// Rate : ピッチ変化の速度 | |
// Depth: ピッチ変化の深さ | |
// Env : エンベロープの種類 | |
// Rate : エンベロープの速度 | |
[0, 0, 50, 0, 0, 50], | |
[1, 0, 50, 0, 0, 50], | |
[2, 0, 50, 0, 0, 50], | |
[3, 0, 50, 0, 0, 50], | |
[4, 0, 50, 0, 0, 50], | |
[0, 0, 50, 0, 1, 50], | |
[1, 0, 50, 0, 1, 50], | |
[2, 0, 50, 0, 1, 50], | |
[3, 0, 50, 0, 1, 50], | |
[4, 0, 50, 0, 1, 50], | |
[3, 0, 50, 0, 1, 16], | |
[1, 1, 25, 100, 1, 25], | |
[1, 2, 25, 100, 1, 25], | |
[0, 0, 50, 100, 0, 50], | |
[1, 0, 50, 25, 0, 50], | |
[2, 0, 50, 25, 0, 50], | |
[0, 0, 50, 0, 0, 50], | |
[1, 0, 50, 0, 0, 50], | |
[2, 0, 50, 0, 0, 50], | |
[3, 0, 50, 0, 0, 50], | |
[4, 0, 50, 0, 0, 50], | |
[0, 0, 50, 0, 1, 50], | |
[1, 0, 50, 0, 1, 50], | |
[2, 0, 50, 0, 1, 50], | |
[3, 0, 50, 0, 1, 50], | |
[4, 0, 50, 0, 1, 50], | |
[0, 1, 50, 0, 1, 50], | |
[1, 1, 50, 0, 1, 50], | |
[2, 1, 50, 0, 1, 50], | |
[3, 1, 50, 0, 1, 50], | |
[4, 1, 50, 0, 1, 50], | |
[0, 1, 50, 100, 1, 50], | |
[0, 0, 50, 0, 0, 50], | |
[1, 0, 50, 0, 0, 50], | |
[2, 0, 50, 0, 0, 50], | |
[3, 0, 50, 0, 0, 50], | |
[4, 0, 50, 0, 0, 50], | |
[0, 0, 50, 0, 1, 50], | |
[1, 0, 50, 0, 1, 50], | |
[2, 0, 50, 0, 1, 50], | |
[3, 0, 50, 0, 1, 50], | |
[4, 0, 50, 0, 1, 50], | |
[0, 1, 50, 0, 1, 50], | |
[1, 1, 50, 0, 1, 50], | |
[2, 1, 50, 0, 1, 50], | |
[3, 1, 50, 0, 1, 50], | |
[4, 1, 50, 0, 1, 50], | |
[0, 1, 50, 100, 1, 50], | |
[0, 0, 50, 0, 0, 50], | |
[1, 0, 50, 0, 0, 50], | |
[2, 0, 50, 0, 0, 50], | |
[3, 0, 50, 0, 0, 50], | |
[4, 0, 50, 0, 0, 50], | |
[0, 0, 50, 0, 1, 50], | |
[1, 0, 50, 0, 1, 50], | |
[2, 0, 50, 0, 1, 50], | |
[3, 0, 50, 0, 1, 50], | |
[4, 0, 50, 0, 1, 50], | |
[0, 1, 50, 0, 1, 50], | |
[1, 1, 50, 0, 1, 50], | |
[2, 1, 50, 0, 1, 50], | |
[3, 1, 50, 0, 1, 50], | |
[4, 1, 50, 0, 1, 50], | |
[0, 1, 50, 100, 1, 50], | |
[0, 0, 50, 0, 0, 50], | |
[1, 0, 50, 0, 0, 50], | |
[2, 0, 50, 0, 0, 50], | |
[3, 0, 50, 0, 0, 50], | |
[4, 0, 50, 0, 0, 50], | |
[0, 0, 50, 0, 1, 50], | |
[1, 0, 50, 0, 1, 50], | |
[2, 0, 50, 0, 1, 50], | |
[3, 0, 50, 0, 1, 50], | |
[4, 0, 50, 0, 1, 50], | |
[0, 1, 50, 0, 1, 50], | |
[1, 1, 50, 0, 1, 50], | |
[2, 1, 50, 0, 1, 50], | |
[3, 1, 50, 0, 1, 50], | |
[4, 1, 50, 0, 1, 50], | |
[0, 1, 50, 100, 1, 50], | |
[0, 0, 50, 0, 0, 50], | |
[1, 0, 50, 0, 0, 50], | |
[2, 0, 50, 0, 0, 50], | |
[3, 0, 50, 0, 0, 50], | |
[4, 0, 50, 0, 0, 50], | |
[0, 0, 50, 0, 1, 50], | |
[1, 0, 50, 0, 1, 50], | |
[2, 0, 50, 0, 1, 50], | |
[3, 0, 50, 0, 1, 50], | |
[4, 0, 50, 0, 1, 50], | |
[0, 1, 50, 0, 1, 50], | |
[1, 1, 50, 0, 1, 50], | |
[2, 1, 50, 0, 1, 50], | |
[3, 1, 50, 0, 1, 50], | |
[4, 1, 50, 0, 1, 50], | |
[0, 1, 50, 100, 1, 50], | |
[0, 0, 50, 0, 0, 50], | |
[1, 0, 50, 0, 0, 50], | |
[2, 0, 50, 0, 0, 50], | |
[3, 0, 50, 0, 0, 50], | |
[4, 0, 50, 0, 0, 50], | |
[0, 0, 50, 0, 1, 50], | |
[1, 0, 50, 0, 1, 50], | |
[2, 0, 50, 0, 1, 50], | |
[3, 0, 50, 0, 1, 50], | |
[4, 0, 50, 0, 1, 50], | |
[0, 1, 50, 0, 1, 50], | |
[1, 1, 50, 0, 1, 50], | |
[2, 1, 50, 0, 1, 50], | |
[3, 1, 50, 0, 1, 50], | |
[4, 1, 50, 0, 1, 50], | |
[0, 1, 50, 100, 1, 50], | |
[0, 0, 50, 0, 0, 50], | |
[1, 0, 50, 0, 0, 50], | |
[2, 0, 50, 0, 0, 50], | |
[3, 0, 50, 0, 0, 50], | |
[4, 0, 50, 0, 0, 50], | |
[0, 0, 50, 0, 1, 50], | |
[1, 0, 50, 0, 1, 50], | |
[2, 0, 50, 0, 1, 50], | |
[3, 0, 50, 0, 1, 50], | |
[4, 0, 50, 0, 1, 50], | |
[0, 1, 50, 0, 1, 50], | |
[1, 1, 50, 0, 1, 50], | |
[2, 1, 50, 0, 1, 50], | |
[3, 1, 50, 0, 1, 50], | |
[4, 1, 50, 0, 1, 50], | |
[0, 1, 50, 100, 1, 50], | |
]; | |
// WebMidiLink用のコード | |
// * http://www.g200kg.com/en/docs/webmidilink/spec.html | |
// http://d.hatena.ne.jp/aike/20120628 | |
// 簡単に説明すると外部から postMessage を経由して WebBeeper を操作できる | |
if (window.addEventListener) { | |
window.addEventListener("message", webMidiLinkRecv, false); | |
} | |
// postMessage 時に呼ばれる | |
// * "midi,90,40,64" みたいな文字列を期待している(数値の部分は16進数) | |
function webMidiLinkRecv(event) { | |
var msg = event.data.split(","); | |
switch (msg[0]) { | |
case "midi": | |
// 上位4ビットがコマンドの種類 | |
var m = parseInt(msg[1], 16) & 0xf0; | |
// 下位4ビットがチャンネル | |
var ch = parseInt(msg[1], 16) & 0x0f; | |
// 引数 | |
var arg1 = parseInt(msg[2], 16); | |
var arg2 = parseInt(msg[3], 16); | |
switch (m) { | |
case 0x80: // ノートオフ | |
// ch:チャンネル, arg1:オフにする音の番号 | |
app.NoteOff(ch, arg1); | |
break; | |
case 0x90: // ノートオン | |
if (arg2 > 0) | |
// ch:チャンネル, arg1:オンにする音の番号, arg2:ボリューム | |
app.NoteOn(ch, arg1, arg2); | |
else | |
// 引数省略時はノートオフとする | |
// ch:チャンネル, arg1:オフにする音の番号 | |
app.NoteOff(ch, arg1); | |
break; | |
case 0xb0: | |
switch (arg1) { | |
case 0x07: // CC#7: ボリューム | |
// ch:チャンネル, arg2:ボリューム | |
app.Vol(ch, arg2); | |
break; | |
case 0x78: // MM#120: オールサウンドオフ | |
// ch:チャンネル | |
app.AllSoundOff(ch); | |
break; | |
} | |
break; | |
case 0xc0: // プログラム(音色)チェンジ | |
// ch:チャンネル, arg1:音色の番号 | |
app.SetPrg(ch, arg1); | |
break; | |
} | |
break; | |
} | |
} | |
// オーディオインターフェイス | |
function AudioIf() { | |
this.kcnt = 0; // 10msecカウンタ | |
this.start = 0; // オーディオ処理時は 1 | |
this.written = 0; // 書き込んだサンプル数 (使っていない) | |
this.enable = 0; // 0:None, 1:AudioDataAPI, 2:WebAudioAPI | |
this.totalwritten = 0; // 書き込んだサンプル数 (AudioDataAPIの時に使う) | |
this.count = 0; // (使っていない) | |
this.timer = null; // setInterval用 (AudioDataAPIの時に使う) | |
// WebAudioAPI が使えるか? (Chrome/Safari6) | |
if (typeof (webkitAudioContext) != "undefined") { | |
this.enable = 2; | |
// オーディオコンテキスト | |
// WebAudioAPI のオブジェトは全てこのオブジェクトから生成される | |
this.audio = new webkitAudioContext(); | |
// WebAudioAP Iの場合、サンプリングレートはAPIが決定する | |
samplerate = this.audio.sampleRate; | |
// 信号データを格納するバッファのサイズ | |
outbufsize = 1024; | |
} | |
// AudioDataAPI が使えるか? (Firefox) | |
else if (typeof (Audio) == "function") { | |
this.audio = new Audio(); | |
if (typeof (this.audio.mozSetup) == "function") { | |
this.enable = 1; | |
// AudioDataAPI の場合、サンプリングレートはプログラムで決定する | |
samplerate = 44100; | |
// 信号データを格納するバッファのサイズ | |
// 左右のデータを交互にセットするためバッファのサイズは2倍になる | |
// ステレオで考えると 1024 サンプルなので上記の WebAudioAPI と同じとなる | |
outbufsize = 2048; | |
} | |
} | |
// どちらも使えない場合 | |
if (this.enable == 0) { | |
samplerate = 44100; | |
outbufsize = 4096; | |
} | |
// サンプリングレートの逆数 (1サンプルは何秒か?) | |
// 44.1kHzの場合、0.000022675736961451248秒 | |
rsamplerate = 1 / samplerate; | |
// 信号データを格納するバッファ (*1) | |
// (ourbufsize / samplerate)秒分のデータが格納される | |
try { | |
outbuf = new Float32Array(outbufsize); | |
} catch (e) { | |
outbuf = new Array(outbufsize); | |
} | |
// 初期化? | |
// * 何故乱数で初期化されているのかは分からない | |
// オーディオ処理を書く前のとりあえず音が出るかどうかの確認の名残り?? | |
for (var i = 0; i < outbufsize; ++i) | |
outbuf[i] = Math.random()-0.5; | |
// AudioDataAPI(Firefox)の初期化 | |
// ステレオ(2ch) で samplerate(=44.1kHz)で初期化している | |
if (this.enable == 1) | |
this.audio.mozSetup(2, samplerate); | |
// WebAudioAPI(Chrome)の初期化 | |
if (this.enable == 2) { | |
// JavaScriptで信号処理をするためのノードを生成 | |
// input=2ch, output=2ch を設定している | |
// * このプログラムではインプットは使用していないけど、 | |
// インプット数を 0 にしてしまうと動かなくなってしまう (前はそんなことなかったのに..) | |
this.jsnode = this.audio.createJavaScriptNode(outbufsize, 2, 2); | |
// オーティオ処理を開始したらこの関数が呼び出される | |
this.jsnode.onaudioprocess = function(e) { | |
// 左右のチャンネル用バッファ(Float32Array)を取得 | |
var outl = e.outputBuffer.getChannelData(0); | |
var outr = e.outputBuffer.getChannelData(1); | |
// オーティオ処理(ここで音を作っている) | |
audioif.MakeWave(); | |
// 信号データの書き込み | |
// * 左右で同じデータを書き込んでいるので実質的にはモノラル | |
// ステレオ処理をしたい場合はそれぞれ異なるデータを書き込む | |
outl.set(outbuf); | |
outr.set(outbuf); | |
} | |
} | |
// AudioDataAPI(Firefox)の時にオーティオ処理を開始したらこの関数が呼び出される | |
this.Write = function() { | |
if (audioif.enable == 1) { | |
// 処理を開始していない場合 | |
if (audioif.start == 0) | |
return; | |
// 処理のタイミング制御。先に進み過ぎないように調整している。 | |
// 環境によって mozCurrentSampleOffset() の更新ラグがあるので注意 | |
// SEE ALSO: http://qiita.com/items/bec1ddd92c96deba6e81 | |
var offset = audioif.audio.mozCurrentSampleOffset(); | |
if (offset > 0 && audioif.totalwritten > offset + 16384) | |
return; | |
// オーティオ処理(ここで音を作っている) | |
audioif.MakeWave(); | |
// 信号データの書き込み | |
// AudioDataAPI はひとつの Float32Array 配列に左右のデータを交互に書き込む | |
var w = audioif.audio.mozWriteAudio(outbuf); | |
// 書き込んだサンプル数 | |
audioif.totalwritten += w; | |
audioif.written += w; | |
} | |
} | |
// オーティオ処理の開始 | |
this.Start = function() { | |
this.start = 1; | |
switch (this.enable) { | |
case 1: | |
if (this.timer == null) | |
// AudioDataAPI(Firefox)は自分でタイマー制御しないといけない *2 | |
this.timer = setInterval(this.Write, 20); | |
break; | |
case 2: | |
// WebAudioAPI(Chrome)はノードをコンテキストに接続すると開始する | |
// jsnode -> this.audio.destination | |
// * ここで間に DelayNode を挟んだりするとディレイをかけたりできる | |
// jsnode -> DelayNode -> GainNode -> this.audio.destination | |
// -> -> this.audio.destination | |
// Example: | |
// var delay = this.audio.createDelayNode(); | |
// delay.delayTime.value = 0.125; | |
// var gain1 = this.audio.createGainNode(); | |
// gain1.gain.value = 0.4; | |
// this.jsnode.connect(delay); | |
// delay.connect(gain1); | |
// gain1.connect(this.audio.destination); | |
this.jsnode.connect(this.audio.destination); | |
break; | |
} | |
} | |
// オーティオ処理の停止 | |
this.Stop = function() { | |
this.start = 0; | |
switch (this.enable) { | |
case 1: | |
if (this.timer != null) | |
// AudioDataAPI(Firefox)は自分でタイマー制御しないといけない | |
clearInterval(this.timer); | |
// this.timer = null; が抜けている?? | |
break; | |
case 2: | |
// WebAudioAPI(Chrome)はノードをコンテキストから切断すると停止する | |
this.jsnode.disconnect(0); | |
break; | |
} | |
} | |
// オーティオ処理 | |
this.MakeWave = function() { | |
var sz = outbuf.length; | |
var v; | |
for (var i = 0; i < sz; ++i) { | |
v = 0; | |
// 10msec ごとに音量やピッチの変化を適用する | |
// * 10msecの解像度になっているのは | |
// 必要以上に細かく処理しても処理効率が下るだけであまり効果はないから | |
if ((this.kcnt += rsamplerate) >= 0.01) { | |
this.kcnt -= 0.01; | |
for (var j = 0; j < 8; ++j) | |
app.oscs[j].IntProcess(); | |
} | |
// 1サンプル分の処理 | |
// 各オシレーターで処理を行なった結果を加算したものが最終的な信号データになる | |
for (var j = 0; j < 8; ++j) { | |
v += app.oscs[j].Process(); | |
} | |
outbuf[i] = v; | |
// AudioDataAPI(Firefox)は左右交互にデータを作る | |
if (this.enable != 2) { | |
outbuf[++i] = v; | |
} | |
} | |
} | |
} // AudioIf オーディオインターフェイス | |
var ncnt = 0; // (使ってない) | |
// オシレーター (音を出すための部品) | |
function Osc() { | |
this.ch = 0; // チャンネル番号 | |
this.note = -1; // 音の高さ(-1のとき発音しない) | |
this.trig = 0; // (使ってない) | |
this.phase = 0; // 位相 | |
this.freq = 0; // 周波数 (実際は位相増分として使っている) | |
this.curlv = 0; // 実際のボリューム | |
this.lv = 0; // 目標のボリューム (目標のボリュームに向って this.curlv が変化する) | |
this.rate = 0; // (使ってない) | |
this.pmod = 1; // ピッチの変化量 | |
this.penvcnt = 0; // ピッチ変化のインデックス | |
this.envcnt = 15; // エンベロープ変化のインデックス | |
this.noise = -0.25; // ノイズジェネレータの現在値 | |
this.prg = prgtable[0]; // プログラム設定 | |
this.wave = new Array(8); // 音の波形テーブル | |
this.penv = new Array(3); // ピッチ変化用の波形テーブル | |
this.env = new Array(2); // エンベロープ用の波形テーブル | |
// 音の波形の定義 | |
// 数値をエクセルに貼りつけて棒グラフにするとどんな波形か分かりやすいです | |
// ウェーブテーブルシンセシス | |
// http://ja.wikipedia.org/wiki/%E6%B3%A2%E5%BD%A2%E3%83%A1%E3%83%A2%E3%83%AA%E9%9F%B3%E6%BA%90 | |
// 1周期分の波形をあらかじめ用意して信号処理時には | |
// 配列のインデックス(位相)を移動させながら値を参照する | |
// 880Hzの音程の場合、1秒間に配列の頭から末尾までを880回走査する | |
// 矩形波(Duty=12.5%) | |
this.wave[0] = [+0.25, +0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, | |
-0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25]; | |
// 矩形波(Duty=25.0%) | |
this.wave[1] = [+0.25, +0.25, +0.25, +0.25, -0.25, -0.25, -0.25, -0.25, | |
-0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25]; | |
// 矩形波(Duty=50.0%) | |
this.wave[2] = [+0.25, +0.25, +0.25, +0.25, +0.25, +0.25, +0.25, +0.25, | |
-0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25]; | |
// 三角波 | |
this.wave[3] = [0, +0.125, +0.25, +0.375, +0.5, +0.375, +0.25, +0.125, | |
0, -0.125, -0.25, -0.375, -0.5, -0.375, -0.25, -0.125]; | |
// ピッチ変化用の波形の定義 | |
// (サンプル数、繰り返し時の再開インデックス、波形...) | |
// 音程が上下する(ビブラート) | |
this.penv[0] = [16,0, | |
0, +0.25, +0.5, +0.75, +1, +0.75, +0.5, +0.25, | |
0, -0.25, -0.5, -0.75, -1, -0.75, -0.5, -0.25]; | |
// 音程が上がる(最終的に目標の音程になる) | |
this.penv[1] = [16,15, | |
-1.00, -0.93, -0.87, -0.80, -0.73, -0.67, -0.60, -0.53, | |
-0.47, -0.40, -0.30, -0.27, -0.20, -0.13, -0.07, +0.00]; | |
// 音程が下がる(最終的に目標の音程になる) | |
this.penv[2] = [16,15, | |
1.00, 0.93, 0.87, 0.80, 0.73, 0.67, 0.60, 0.53, | |
0.47, 0.40, 0.30, 0.27, 0.20, 0.13, 0.07, 0.00]; | |
// エンベロープ用の波形の定義 | |
// (サンプル数、繰り返し時の再開インデックス、波形...) | |
// 持続音 | |
this.env[0] = [1,0, | |
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; | |
// 減衰音 | |
this.env[1] = [15,15, | |
1.00, 0.93, 0.87, 0.80, 0.73, 0.67, 0.60, 0.53, | |
0.47, 0.40, 0.30, 0.27, 0.20, 0.13, 0.07, 0.00]; | |
// 発音するときに呼ばれる(オシレータのリセット) | |
this.Set = function(ch, n) { | |
this.ch = ch; | |
this.note = n; | |
if (n >= 0) { | |
// freq (=周波数)となっているが、実際は周波数をサンプルレートで除算したもの | |
// これは 1サンプルごとの位相の変化量となる | |
// (440 * Math.pow(2, (n - 69) / 12)) | |
// この計算式で n=69 のときに 440Hz を基準に | |
// 1増加するたびに半音高くなる周波数を求めることができる | |
this.freq = (440 * Math.pow(2, (n - 69) / 12)) / samplerate; | |
// たぶん不要なコード (this.rate is NaN) | |
this.prg = prgtable[app.chprg[ch] & 0xf]; | |
this.rate = Math.pow(1000, this.prg.rate / 128); | |
// ピッチ変化のインデックス, エンベロープ変化のインデックスのリセット | |
this.penvcnt = this.envcnt = 0; | |
} | |
} | |
// 10msec毎に呼ばれる | |
// * 10msecの解像度になっているのは | |
// 必要以上に細かく処理しても処理効率が下るだけであまり効果はないから | |
this.IntProcess = function() { | |
// ピッチ変化のインデックスを更新 | |
this.penvcnt += this.prg[2] * 0.01; | |
// 可読性のため変数に代入しています | |
var penv = this.penv[this.prg[1]]; | |
// ピッチ波形テーブルを最後まで読んだ時の調整 | |
// penv[1] が 0 の時、先頭から繰り返し | |
// penv[1] が 15 の時、末尾から繰り返し(最後の高さで持続する) | |
while (this.penvcnt >= penv[0]) { | |
this.penvcnt = this.penvcnt - penv[0] + penv[1]; | |
} | |
// ピッチの変化量の設定 | |
// 音の周波数は 2倍 になれば 1オクターブ 高くなる : Math.pow(2, 1) | |
// 元の周波数 * Math.pow(2, 1/12) だと(12音階で)半音高くなる | |
// ここでは | |
// ピッチ波形テーブルの値 * ピッチ変化の深さ(this.prg[3]以下) | |
// を計算してピッチの変化量を計算している | |
// 0.0078125 = 1/128 (128段階) | |
this.pmod = Math.pow(2, penv[(this.penvcnt + 2) | 0] * this.prg[3] * 0.0078125); | |
// エンベロープ変化のインデックスを更新 | |
this.envcnt += this.prg[5] * 0.01; | |
// 可読性のため変数に代入しています | |
var env = this.env[this.prg[4]]; | |
// ノートオン状態のとき | |
if (this.note >= 0) { | |
// エンベロープ波形テーブルを最後まで読んだ時の調整 | |
// env[1] が 0 の時、先頭から繰り返し | |
// env[1] が 15 の時、末尾から繰り返し(最後の音量で持続する) | |
if (this.envcnt >= env[0]) { | |
this.envcnt = env[1]; | |
} | |
} | |
// エンベロープ変化のインデックスが波形テーブルの末尾を超えたの調整(たぶん無くても良い) | |
if (this.envcnt > 15) | |
this.envcnt = 15; | |
// 目標のボリュームの設定 | |
this.lv = env[Math.floor(this.envcnt + 2)]; | |
} | |
// 1サンプルごとに呼ばれる | |
this.Process = function() { | |
// 実際のボリュームの変化 | |
// 目標のボリュームのに向って 0.01 ずつ変化している | |
if (this.curlv > this.lv) { | |
if ((this.curlv -= 0.01) < 0.01) { | |
this.ena = this.curlv = 0; | |
} | |
} | |
else if (this.curlv < this.lv) { | |
if ((this.curlv += 0.01) > 0) | |
this.ena = 1; | |
} | |
// this.ena > 0 なら 発音中 | |
if (this.ena > 0) { | |
// 波形テーブル用のインデックス(位相)の変化 | |
// this.freq は 周波数ではなく、周波数に応じた位相の変化量 | |
// それを this.pmod で調整している | |
// (変化量を大きくすると音が高くなり、小さくすると音が低くなる) | |
this.phase += (this.freq * this.pmod); | |
if (this.prg[0] == 4) { | |
// ノイズの処理 | |
// 位相 [0..1) の調整 | |
// 波形テーブルの1/4分進んだら乱数値(2値)を更新する (なるほど!!) | |
while (this.phase >= 0.25) { | |
this.phase -= 0.25; | |
this.noise = (Math.random() >= 0.5 ? 0.25 : -0.25); | |
} | |
// 乱数値とチャンネルのボリューム、エンベロープの音量を積算 | |
return this.noise * app.chvol[this.ch] * this.curlv; | |
} | |
else { | |
// 波形テーブルから読み込む処理 | |
// 位相 [0..1) の調整 | |
while (this.phase >= 1) | |
this.phase -= 1; | |
// 波形テーブルから値を読み込む | |
// (どの波形テーブルの何番目の値か) | |
var v = this.wave[this.prg[0]][(this.phase * 16) | 0]; | |
// 読み込んだ値とチャンネルのボリューム、エンベロープの音量を積算 | |
return v * app.chvol[this.ch] * this.curlv; | |
} | |
} | |
return 0; | |
} | |
} // Osc オシレーター | |
// 設定パラメータ | |
// GUI経由でパラメータ prgtable の値を変化させる | |
// (GUIの部分は説明を省きます) | |
function Params() { | |
this.param = new Array(); | |
this.drag = -1; | |
this.orgy = this.orgv = 0; | |
this.prg = 0; // 設定プログラムのインデックス | |
this.Add = function(n, img, x, y, max) { | |
this.param.push({ pnum: n, img: img, x: x, y: y, max: max }); | |
} | |
this.SetPrg = function(app,n) { | |
this.prg = n; | |
this.Draw(app); | |
} | |
this.Draw = function(app) { | |
var p = prgtable[this.prg]; | |
for (var i = 0; i < this.param.length; ++i) { | |
app.ctx.drawImage(this.param[i].img, 0, p[this.param[i].pnum] * 16, 32, 16, this.param[i].x, this.param[i].y, 32, 16); | |
} | |
} | |
this.HitTest = function(x, y) { | |
for (var i = 0; i < this.param.length; ++i) { | |
if (x >= this.param[i].x && x < this.param[i].x + 32 && y >= this.param[i].y && y < this.param[i].y + 16) { | |
return i; | |
} | |
} | |
return -1; | |
} | |
this.MouseDown = function(x, y) { | |
this.drag = this.HitTest(x, y); | |
if (this.drag >= 0) { | |
this.orgy = y; | |
this.orgv = prgtable[this.prg][this.drag]; | |
} | |
} | |
this.MouseMove = function(x, y) { | |
if (this.drag >= 0) { | |
var v = (this.orgv - (y - this.orgy) * this.param[this.drag].max * 0.01); | |
if (v < 0) | |
v = 0; | |
if (v > this.param[this.drag].max - 1) | |
v = this.param[this.drag].max - 1; | |
prgtable[this.prg][this.drag] = Math.floor(v); | |
this.Draw(app); | |
} | |
} | |
this.MouseUp = function(x, y) { | |
this.drag = -1; | |
} | |
} // Params 設定パラメータ | |
// アプリケーション | |
// GUI経由でアプリケーション全体を制御している | |
// (GUIの部分は説明を省きます) | |
function App() { | |
this.ctx = canvas.getContext("2d"); | |
this.imgkeys = document.getElementById("keys"); | |
this.imgval = document.getElementById("val"); | |
this.imgval2=document.getElementById("val2"); | |
this.imgch = document.getElementById("ch"); | |
this.imgform = document.getElementById("form"); | |
this.imgpenv = document.getElementById("penv"); | |
this.imgenv = document.getElementById("env"); | |
this.ctx.drawImage(this.imgkeys, 0, 0); | |
this.oscs = new Array(8); // オシレータ (8音同時発音) | |
this.chvol = new Array(16); // チャンネルごとのボリューム | |
this.chprg = new Array(16); // チャンネルごとのプログラム設定 | |
this.mousex = this.mousey = this.mousepress = 0; | |
this.curch = 0; | |
this.orgy = 0; | |
this.orgv = 0; | |
this.lastnote = 0; | |
this.params = new Params(); | |
this.params.Add(0, this.imgform, 65, 200,5); | |
this.params.Add(1, this.imgpenv, 115, 200,3); | |
this.params.Add(2, this.imgval2, 150, 200,101); | |
this.params.Add(3, this.imgval2, 185, 200,101); | |
this.params.Add(4, this.imgenv, 240, 200,2); | |
this.params.Add(5, this.imgval2, 275, 200,101); | |
for (var i = 0; i < 8; ++i) { | |
this.oscs[i] = new Osc(); | |
} | |
// ノートオン | |
this.NoteOn = function(ch, n, v) { | |
if (n >= 0 && n < 128) { | |
this.ctx.fillStyle = "#ff0000"; | |
this.ctx.fillRect(n * 3 + 16, 64 + ch * 8, 3, 8); | |
// 空いているオシレータを探してオシレータをリセットする | |
for (var i = 0; i < 8; ++i) { | |
if (this.oscs[i].note < 0) { // note が -1 なら発音していない | |
this.oscs[i].Set(ch, n); // オシレータのリセット | |
break; | |
} | |
} | |
} | |
} | |
// ノートオフ | |
this.NoteOff = function(ch, n) { | |
if (n >= 0 && n < 128) { | |
this.ctx.drawImage(this.imgkeys, n * 3 + 16, 64 + ch * 8, 3, 8, n * 3 + 16, 64 + ch * 8, 3, 8); | |
for (var i = 0; i < 8; ++i) { | |
if (this.oscs[i].ch == ch && this.oscs[i].note == n) | |
this.oscs[i].Set(ch, -1); | |
} | |
} | |
} | |
// ボリュームチェンジ | |
this.Vol = function(ch, v) { | |
this.ctx.drawImage(this.imgval, 0, v * 8, 16, 8, 400, ch * 8 + 64, 16, 8); | |
this.chvol[ch] = v / 127; | |
} | |
this.GetVol = function(ch) { | |
return this.chvol[ch] * 127; | |
} | |
// プログラムチェンジ | |
this.SetPrg = function(ch, p) { | |
this.ctx.drawImage(this.imgval, 0, p * 8, 16, 8, 416, ch * 8 + 64, 16, 8); | |
this.chprg[ch] = p; | |
this.ShowParam(); | |
} | |
// チャンネルチェンジ | |
this.SetCh = function(ch) { | |
this.ctx.drawImage(this.imgkeys, 0, this.curch * 8 + 64, 16, 8, 0, 64 + this.curch * 8, 16, 8); | |
this.ctx.drawImage(this.imgch, 0, ch * 8, 16, 8, 0, 64 + ch * 8, 16, 8); | |
this.curch = ch; | |
this.ShowParam(); | |
} | |
// パラメータの表示 | |
this.ShowParam = function() { | |
var prg = this.chprg[this.curch] & 0xf; | |
this.params.SetPrg(this, prg); | |
this.ctx.drawImage(this.imgval, 0, prg * 8, 16, 8, 40, 200, 16, 8); | |
this.params.Draw(this); | |
} | |
// 全音停止 | |
this.AllSoundOff = function(ch) { | |
this.ctx.drawImage(this.imgkeys, 16, 64 + ch * 8, 384, 8, 16, 64 + ch * 8, 384, 8); | |
for (var i = 0; i < 8; ++i) { | |
if (this.oscs[i].note >= 0 && this.oscs[i].ch == ch) | |
this.oscs[i].Set(ch, -1); | |
} | |
} | |
// マウス座標の計算 | |
this.getXY=function(e) { | |
var rc = e.target.getBoundingClientRect(); | |
this.mousex = Math.floor(e.clientX - rc.left); | |
this.mousey = Math.floor(e.clientY - rc.top); | |
if (this.mousex < 0) this.mousex = 0; | |
if (this.mousey < 0) this.mousey = 0; | |
} | |
// マウス押下時、座標に応じて音を出したり設定を変えたりする | |
this.MouseDown = function(e) { | |
this.getXY(e); | |
var ch = ((this.mousey - 64) / 8) | 0; | |
this.orgy = this.mousey; | |
if (this.mousey >= 32 && this.mousey < 64 && this.mousex >= 16 && this.mousex < 400) { | |
var n; | |
if (this.mousey <= 52) | |
n = ((this.mousex - 16) / 3) | 0; | |
else { | |
n = ((this.mousex - 16) / 5.142857142857143) | 0; | |
n = ((n / 7) | 0) * 12 + [0, 2, 4, 5, 7, 9, 11][n % 7]; | |
} | |
this.lastnote = n; | |
this.NoteOn(this.curch, n, 64); | |
this.mousepress = 4; | |
} | |
if (this.mousey>=64 && ch >= 0 && ch < 16) { | |
this.SetCh(ch); | |
if (this.mousex >= 16 && this.mousex < 400) { | |
this.lastnote = ((this.mousex - 16) / 3) | 0; | |
this.NoteOn(this.curch, this.lastnote, 64); | |
this.mousepress = 3; | |
} | |
if (this.mousex >= 400 && this.mousex < 416) { | |
this.orgv = this.GetVol(this.curch); | |
this.mousepress = 1; | |
} | |
if (this.mousex >= 416 && this.mousex < 432) { | |
this.orgv = this.chprg[this.curch]; | |
this.mousepress = 2; | |
} | |
} | |
this.params.MouseDown(this.mousex, this.mousey); | |
} | |
// マウス移動時、押しっぱで移動したときの音や値の反映とか | |
this.MouseMove = function(e) { | |
this.getXY(e); | |
switch (this.mousepress) { | |
case 1: | |
var val = (this.orgv - (this.mousey - this.orgy) * 0.5) | 0; | |
if (val < 0) val = 0; | |
if (val > 127) val = 127; | |
this.Vol(this.curch, val); | |
break; | |
case 2: | |
var val = (this.orgv - (this.mousey - this.orgy) * 0.5) | 0; | |
if (val < 0) val = 0; | |
if (val > 127) val = 127; | |
this.SetPrg(this.curch, val); | |
break; | |
case 3: | |
var n = ((this.mousex - 16) / 3) | 0; | |
var ch = ((this.mousey - 64) / 8) | 0; | |
if (ch < 0) ch = 0; | |
if (ch >= 16) ch = 15; | |
if (n != this.lastnote || ch != this.curch) { | |
this.NoteOff(this.curch, this.lastnote); | |
this.SetCh(ch); | |
this.NoteOn(this.curch, n, 64); | |
this.lastnote = n; | |
} | |
break; | |
case 4: | |
var n; | |
if (this.mousey <= 52 || this.mousey >= 64) | |
n = ((this.mousex - 16) / 3) | 0; | |
else { | |
n = ((this.mousex - 16) / 5.142857142857143) | 0; | |
n = ((n / 7) | 0) * 12 + [0, 2, 4, 5, 7, 9, 11][n % 7]; | |
} | |
if (n != this.lastnote) { | |
this.NoteOff(this.curch, this.lastnote); | |
this.NoteOn(this.curch, n, 64); | |
this.lastnote = n; | |
} | |
break; | |
} | |
this.params.MouseMove(this.mousex, this.mousey); | |
} | |
this.MouseUp = function(e) { | |
if (this.mousepress == 3 || this.mousepress == 4) | |
this.NoteOff(this.curch, this.lastnote); | |
this.mousepress = 0; | |
this.params.MouseUp(this.mousex, this.mousey); | |
} | |
// 初期化 | |
for (var i = 0; i < 16; ++i) { | |
this.Vol(i, 64); | |
this.SetPrg(i, i); | |
} | |
} // App アプリケーション | |
function MouseDown(e) { | |
app.MouseDown(e); | |
} | |
function MouseMove(e) { | |
app.MouseMove(e); | |
} | |
function MouseUp(e) { | |
app.MouseUp(e); | |
} | |
// 初期化(最初に呼ばれる) | |
function Init() { | |
canvas = document.getElementById("canvas"); | |
audioif = new AudioIf(); | |
app = new App(); | |
canvas.onmousedown = MouseDown; | |
canvas.onmousemove = MouseMove; | |
canvas.onmouseup = MouseUp; | |
audioif.Start(); | |
} | |
// 注釈 | |
// *1: 信号データを格納するバッファ | |
// このバッファのサイズ毎に処理を行なうため、そこを埋めている間のイベントはブロックされる | |
// バッファサイズが1024、サンプリングレートが44.1kHzの場合、 | |
// 外部からのイベントは 23.22msec (1024/44100*1000) 単位で処理されることになる。 | |
// | |
// *2: window.setInterval を使った場合、 | |
// ブラウザで別のタブを選択したときにタイマーの精度が落ち、音が途切れる問題があります。 | |
// この問題を解消するには WebWorker を使ったタイマーを使う方法があります。 | |
// http://qiita.com/items/b4f713bf8ab9de8907f1 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment