Firefox4ベータ版でaudio data apiが搭載されてるので記念に音楽ライブラリ未満を書いてみる
(12/6修正)ZeroCrossingWave(10/9修正)TableOsc Sampler Phaser Mixer(9/26)SndOut
前回の記事から間が空き、その間にaudio data apiの仕様が変わりました。詳細は前回の記事に追記しておきます、いずれ!。さて今日はjavascript用の音楽ライブラリ未満を書いてみました。データフローでプロトタイプで継承でオーバーライドでcallでってやってたら再帰の呼び出し制限で引っかかって午前を潰してしまった(すべて自分の力不足)。しょうがないので動く重視で行こうと路線変更をして何とか書いてみた。なにかと変なところがありますが、何かの参考になれば幸いです。されでは引き続きFirefox4をお楽しみください。
機能
- オシレータ(正弦波、矩形波、三角波、のこぎり波、ノイズ、波形テーブル)
- サンプラー
- フィルタ(ローパス、ハイパス、バンドパス、オールパス、MoogVCF)
- イフェクト(ディレイ、ディストーション、フェーザー(重い)、ゼロ交差波)
- ミキサー(加算、乗算、ミキサー)
- シーケンス(リニアだけ)
todo
使い方
正弦波440Hz
var out = new SinOsc(440, 1); var sndout = new SndOut(out); sndout.play();
周波数変調と振幅変調
var fm = new SinOsc(1, 220); var am = new SinOsc(10, 0.3); var out = new SqrOsc(440, 0.5, fm, am); var sndout = new SndOut(out); sndout.play();
ローパスフィルタのカットオフ周波数を変調
var cfm = new SinOsc(10, 1000); //変調 var noise = new Noise(0.8); //入力ソース var out = new LowPass(noise, 3000, 0.9, cfm); //フィルタ var sndout = new SndOut(out) sndout.play();
var seq = new Track([800,0,0], //例えば周波数の値 [ 1,0,0], //時間 [1,2,3,4]) //周波数を整数倍する var osc = new SinOsc(0, 1, seq); var out = new SndOut(osc); out.play();
var seq = new Track([1,1,0],[0.1,0,0][1,0,0,0]); var sampler = new Sampler("sample.ogg",1,seq);//今のとこモノラルだけ var out = new SndOut(sampler); out.play()
ソース
<html> <head> <script type="text/javascript"> var sampleRate = 44100; var AudioDataDestination = function(sampleRate, readFn) { var audio = new Audio(); if(!(audio.mozSetup instanceof Function)) { alert("Audio Data API is not supported. FireFox4!!!!"); } audio.mozSetup(1, sampleRate); var currentWritePosition = 0; var prebufferSize = sampleRate * 0.250; var tail = null; this.stop_flag = null; this.stop_flag = setInterval(function() { var written; if(tail) { written = audio.mozWriteAudio(tail); currentWritePosition += written; if(written < tail.length) { tail = tail.slice(written); return; } tail = null; } var currentPosition = audio.mozCurrentSampleOffset(); var available = currentPosition + prebufferSize - currentWritePosition; if(available > 0) { var soundData = new Float32Array(Math.floor(available)); readFn(soundData); written = audio.mozWriteAudio(soundData); if(written < soundData.length) { tail = soundData.slice(written); } currentWritePosition += written; } }, 10); } var SndOut = function(inobject){ this.sndobj = inobject; var self = this; var destination = null; var snum = 0; var getSoundData = function(data) { for(var i=0;i<data.length;i++){ data[i]=self.sndobj.proc(snum++); snum %= sampleRate; } return null; } this.reobj = function(in_o){ this.sndobj = in_o; } this.play = function(){ destination = new AudioDataDestination(sampleRate, getSoundData); } this.stop = function(){ clearInterval(destination.stop_flag); } } var Proxy = function(n) { this.number = n; } Proxy.prototype.proc = function(n) { return this.number; } var None = new Proxy(0); var Port = function(n){ if (typeof(n) == "number"){ return new Proxy(n); }else{ return n; } } var Gen = function(){ this.freq = Port(0); this.amp = Port(0); this.addfreq = Port(0); this.addamp = Port(0); this.phase = 0; switch (arguments.length) { default: case 4: this.addamp = Port(arguments[3]); case 3: this.addfreq = Port(arguments[2]); case 2: this.amp = Port(arguments[1]); case 1: this.freq = Port(arguments[0]); } } Gen.prototype.setfreq = function() { switch (arguments.length) { default: case 2: this.addfreq = Port(arguments[1]); case 1: this.freq = Port(arguments[0]); } } Gen.prototype.setamp = function() { switch (arguments.length) { default: case 2: this.addamp = Port(arguments[1]); case 1: this.amp = Port(arguments[0]); } } var SinOsc = function() { Gen.apply(this,arguments); } SinOsc.prototype = new Gen(); SinOsc.prototype.proc = function(num) { this.phase += ( this.freq.proc(num) + this.addfreq.proc(num) )/sampleRate; this.phase = this.phase >= 1 ? 0 : this.phase; return Math.sin(2*Math.PI*this.phase)*(this.amp.proc(num) + this.addamp.proc(num)); } var SawOsc = function() { Gen.apply(this,arguments); } SawOsc.prototype = new Gen(); SawOsc.prototype.proc = function(num) { this.phase += ( this.freq.proc(num) + this.addfreq.proc(num) )*2/sampleRate; this.phase = this.phase >= 1 ? -1 : this.phase; return this.phase*(this.amp.proc(num) + this.addamp.proc(num)); } var SqrOsc = function() { Gen.apply(this,arguments); } SqrOsc.prototype = new Gen(); SqrOsc.prototype.proc = function(num) { this.phase += (this.freq.proc(num) + this.addfreq.proc(num))*2/sampleRate; this.phase = (this.phase >= 1.0) ? -1 : this.phase; var d = (this.amp.proc(num) + this.addamp.proc(num)); return this.phase < 0 ? d*-1 : d; } var TriOsc = function() { Gen.apply(this,arguments); } TriOsc.prototype = new Gen(); TriOsc.prototype.proc = function(num) { this.phase += (this.freq.proc(num) + this.addfreq.proc(num))*2/sampleRate; this.phase = (this.phase >= 1) ? -1 : this.phase; var d = this.phase * 2; d = (d > 1) ? 1-(d-1) : d; d = (d < -1) ? -2-d : d; return d*(this.amp.proc(num) + this.addamp.proc(num)); } var TableOsc = function(){ this.wave_array = []; this.amp = Port(0); this.freq = Port(0); this.modfreq = Port(0); this.modamp = Port(0); this.phase = 0; switch (arguments.length) { default: case 5: this.modamp = Port(arguments[4]); case 4: this.modfreq = Port(arguments[3]); case 3: this.amp = Port(arguments[2]); case 2: this.freq = Port(arguments[1]); case 1: this.wave_array = arguments[0]; } } TableOsc.prototype.setfreq = function(){ switch (arguments.length) { default: case 2: this.addfreq = Port(arguments[1]); case 1: this.freq = Port(arguments[0]); } } TableOsc.prototype.setamp = function(){ switch (arguments.length) { default: case 2: this.addamp = Port(arguments[1]); case 1: this.amp = Port(arguments[0]); } } TableOsc.prototype.proc = function(num){ this.phase += (this.freq.proc(num) + this.modfreq.proc(num))/sampleRate; this.phase = (this.phase >= 1) ? 0 : this.phase; return this.wave_array[Math.floor((this.wave_array.length-1)*this.phase)]*(this.amp.proc(num) + this.modamp.proc(num)); } var Noise = function(a){ this.amp = Port(a); this.proc = function(num){ return (Math.random()*2 - 1) * this.amp.proc(num); } } var Sampler = function(file, a, t) { /* * dsp.js */ this.amp = Port(a); this.phase = 0; this.file = file; this.samples = []; this.duration = 0; this.sore = 0; var self = this; this.triger = Port(t); this.loadSamples = function(event) { var buffer = event.frameBuffer; var temp = new Float32Array(buffer.length); for(var i=0; i< buffer.length; i++){ temp[i] = buffer[i]; } for(var j=0; j< temp.length; j++){ self.samples.push(temp[j]); } } this.loadComplete = function() { //$('loading').innerHTML = "ok"; }; this.loadMetaData = function() { self.duration = audio.duration; //$('loading').innerHTML = "now loading"; } var audio = /*new Audio(); */ document.createElement("AUDIO"); audio.src = file; audio.addEventListener("loadedmetadata", this.loadMetaData, false); audio.addEventListener("MozAudioAvailable", this.loadSamples, false); audio.addEventListener("ended", this.loadComplete, false); audio.muted = true; audio.play(); }; Sampler.prototype.proc = function(num) { var temp = 0; if (this.triger.proc(num) > 0){ if (this.phase < this.samples.length ) { temp = this.samples[this.phase] * this.amp.proc(num); } else { temp = 0; } }else{ this.phase = 0; } this.phase = this.phase + 1; return temp; }; var Add = function(a, b){ this.proc = function(num){ return a.proc(num) + b.proc(num); } } var Mult = function(a, b){ this.proc = function(num){ return a.proc(num) * b.proc(num); } } var Mixer = function(){ this.mixarray = []; this.mixlength = 0; for(var i=0; i<arguments.length; i++){ this.mixarray.push(arguments[i]); this.mixlength++; } } Mixer.prototype.proc = function(num) { var temp = 0; for(var i=0; i<this.mixlength; i++){ temp += this.mixarray[i].proc(num); } return temp/this.mixlength; } var Trigers = function(inobject, note, time, op){ var inum = 0; var triger = 1; setInterval(function(){ triger = note[inum]; inum = (inum + 1) % note.length; },time) this.proc = function(num){ return inobject.proc(num) * triger * op; } } var Trigers2 = function(note, time, op){ var inum = 0; var triger = 1; setInterval(function(){ triger = note[inum]; inum = (inum + 1) % note.length; },time) this.proc = function(num){ return triger * op; } } var InterpArray = function(a,b){ /* * imporve (SndObj interp.cpp) */ for (var i=0; i<b.length;i++){ if (b[i] == 0){ b[i] = 1/sampleRate; } } var inum = 0; var count = 0; var temp = 0; this.proc = function(num){ if (count >= b[inum]*sampleRate){ inum = (inum + 1) % a.length; count = 0; return temp; }else{ temp = a[inum] + (a[(inum+1)%a.length] - a[inum])/(b[inum]*sampleRate) * count++; return temp; } } } var Track = function(o,t,c){ var tane = o; var tane_time = t; var tane_oto = c; var tane_array =[]; var tane_time_array = []; for (var i=0; i<tane_oto.length; i++){ if (tane_oto[i] != 0){ for (var j=0; j<tane.length; j++){ tane_array.push(tane[j]*tane_oto[i]); tane_time_array.push(tane_time[j]); } }else{ for (var k=0; k<tane.length; k++){ tane_array.push(0); tane_time_array.push(tane_time[k]); } } } return new InterpArray(tane_array, tane_time_array); } var Chain = function(o,t,c,s){ var tane = o; var tane_time = t; var tane_oto = c; var tane_select = s; var tane_array =[]; var tane_time_array = []; for (var i=0; i<tane_select.length; i++){ for (var k=0; k<tane_oto[tane_select[i]].length; k++){ for (var j=0; j<tane.length; j++){ tane_array.push(tane[j]*tane_oto[tane_select[i]][k]); tane_time_array.push(tane_time[j]); } } } return new InterpArray(tane_array, tane_time_array); } var Filter = function(){ /* * improve (http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt) */ this,in_o = Port(0); this.cf = Port(0); this.q = Port(0); this.addcf = Port(0); this.addq = Port(0); switch (arguments.length) { default: case 5: this.addq = Port(arguments[4]); case 4: this.addcf = Port(arguments[3]); case 3: this.q = Port(arguments[2]); case 2: this.cf = Port(arguments[1]); case 1: this.in_o = Port(arguments[0]); } this.b0 = 0; this.b1 = 0; this.b2 = 0; this.a0 = 0; this.a1 = 0; this.a2 = 0; this.forder = 0; this.sorder = 0; } var LowPass = function() { /* * improve (http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt) * BandPass(source, cutoff_freq, Q) * BandPass(source, cutoff_freq, Q, modulation_cutoff, modulation_Q) */ Filter.apply(this,arguments); } LowPass.prototype = new Filter(); LowPass.prototype.calc = function(num) { var omega = 2 * Math.PI * ( this.cf.proc(num) + this.addcf.proc(num) ) / sampleRate; var alpha = Math.sin(omega)/(2*( this.q.proc(num) + this.addq.proc(num) )); var cs = Math.cos(omega); this.b0 = (1 - cs) / 2; this.b1 = 1 - cs; this.b2 = (1 - cs) / 2; this.a0 = 1 + alpha; this.a1 = -2 * cs; this.a2 = 1 - alpha; } LowPass.prototype.proc = function(num) { this.calc(num); var temp = (this.b0/this.a0)*this.in_o.proc(num) + (this.b1/this.a0)*this.forder + (this.b2/this.a0)*this.sorder - (this.a1/this.a0)*this.forder - (this.a2/this.a0)*this.sorder; this.sorder = this.forder; this.forder = temp; return temp; } var HightPass = function() { /* * improve (http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt) * HightPass(source, cutoff_freq, Q) * HightPass(source, cutoff_freq, Q, modulation_cutoff, modulation_Q) */ Filter.apply(this,arguments); } HightPass.prototype = new Filter(); HightPass.prototype.calc = function(num) { var omega = 2 * Math.PI * (this.cf.proc(num) + this.addcf.proc(num)) / sampleRate; var alpha = Math.sin(omega)/(2*(this.q.proc(num) + this.addq.proc(num))); var cs = Math.cos(omega); this.b0 = (1 + cs) / 2; this.b1 = -(1 + cs); this.b2 = (1 + cs) / 2; this.a0 = 1 + alpha; this.a1 = -2 * cs; this.a2 = 1 - alpha; } HightPass.prototype.proc = function(num) { this.calc(num); var temp = (this.b0/this.a0)*this.in_o.proc(num) + (this.b1/this.a0)*this.forder + (this.b2/this.a0)*this.sorder - (this.a1/this.a0)*this.forder - (this.a2/this.a0)*this.sorder; this.sorder = this.forder; this.forder = temp; return temp; } var BandPass = function() { /* * improve (http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt) * BandPass(source, cutoff_freq, bandWidth) * BandPass(source, cutoff_freq, bandWidth, modulation_cutoff, mod_bandpass) */ Filter.apply(this,arguments); } BandPass.prototype = new Filter(); BandPass.prototype.calc = function(num) { var sinh = function(arg){return (Math.exp(arg) - Math.exp(-arg))/2}; var omega = 2 * Math.PI * (this.cf.proc(num) + this.addcf.proc(num)) / sampleRate; var alpha = Math.sin(omega) * sinh(Math.log(2) / 2 * (this.q.proc(num) + this.addq.proc(num)) * omega / Math.sin(omega)); var cs = Math.cos(omega); this.b0 = alpha; this.b1 = 0; this.b2 = -alpha; this.a0 = 1 + alpha; this.a1 = -2 * cs; this.a2 = 1 - alpha; } BandPass.prototype.proc = function(num) { this.calc(num); var temp = (this.b0/this.a0)*this.in_o.proc(num) + (this.b1/this.a0)*this.forder + (this.b2/this.a0)*this.sorder - (this.a1/this.a0)*this.forder - (this.a2/this.a0)*this.sorder; this.sorder = this.forder; this.forder = temp; return temp; } var AllPass = function(i, f, q){ /* * improve (http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt) */ var omega = 2 * Math.PI * f / sampleRate; var alpha = Math.sin(omega) * (2*q); var cs = Math.cos(omega); var b0 = 1 - alpha; var b1 = -2 * cs; var b2 = 1 + alpha; var a0 = 1 + alpha; var a1 = -2 * cs; var a2 = 1 - alpha; var forder = 0; var sorder = 0; this.proc = function(num){ var temp = (b0/a0)*i.proc(num) + (b1/a0)*forder + (b2/a0)*sorder - (a1/a0)*forder - (a2/a0)*sorder; sorder = forder; forder = temp; return temp; } } var MoogVCF = function(i, c, r){ /* * http://www.musicdsp.org/showArchiveComment.php?ArchiveID=24 */ var f = 2 * c / sampleRate; //[0 - 1] var k = 3.6*f - 1.6*f*f -1; var p = (k+1)*0.5; var scale = Math.E^((1-p)*1.386249); var r = r*scale; var y1=y2=y3=y4=oldx=oldy1=oldy2=oldy3=0; this.proc = function(num){ var x = i.proc(num)- r*y4; y1=x*p + oldx*p - k*y1; y2=y1*p+oldy1*p - k*y2; y3=y2*p+oldy2*p - k*y3; y4=y3*p+oldy3*p - k*y4; y4 = y4 - (y4^3)/6; oldx = x; oldy1 = y1; oldy2 = y2; oldy3 = y3; return y4; } } var Delay = function(inobject, t){ var inum = 0; var count = 0; var delayArray = new Float32Array(Math.floor(t*sampleRate)); for (var i=0; i<delayArray.length; i++){ delayArray[i] = 0; } this.proc = function(num){ var temp = delayArray[inum]; delayArray[inum] = inobject.proc(num); inum = (inum+1) % delayArray.length; return temp; } } var Distortion = function(in_o , threshold){ /* * http://www.musicdsp.org/showArchiveComment.php?ArchiveID=203 */ this.proc = function(num){ var temp = in_o.proc(num); if (temp>threshold || temp<-threshold){ return (Math.abs(Math.abs((temp - threshold) % threshold*4) - threshold*2) - threshold)/threshold; }else{ return temp/threshold; } } } var Phaser = function(in_o, f1, f2, r){ /* * http://www.musicdsp.org/showArchiveComment.php?ArchiveID=78 */ var _alps = new Array(6); for(var i=0; i<_alps.length; i++){ _alps[i] = new AllpassDelay(in_o); } var _dmin = null; var _dmax = null; //range var _lfoInc = null; var _zm1 = null; var _fb = 0.7; //feedback var _lfoPhase = 0; var _depth = 1; var _zm1 = 0; this.Range = function(fMin,fMax ){// Hz _dmin = fMin / (sampleRate/2); _dmax = fMax / (sampleRate/2); } this.Rate = function(rate){ // cps _lfoInc = 2 * 2*Math.PI * (rate / sampleRate); } this.Feedback = function(fb){ // 0 -> <1. _fb = fb; } this.Depth = function(depth){ // 0 -> 1. _depth = depth; } //this.Range( 440, 1600 ); //this.Rate( .5 ); this.Range( f1, f2 ); this.Rate( r ); this.proc = function( num ){ //calculate and update phaser sweep lfo... d = _dmin + (_dmax-_dmin) * ((Math.sin( _lfoPhase ) + 1)/2); _lfoPhase += _lfoInc; if( _lfoPhase >= 2*Math.PI * 2 ){ _lfoPhase -= 2*Math.PI * 2; } //update filter coeffs for(var i=0; i<6; i++ ){ _alps[i].Delay( d ); } //calculate output y = _alps[0].proc( _alps[1].proc( _alps[2].proc( _alps[3].proc( _alps[4].proc( _alps[5].proc( in_o.proc(num) + _zm1 * _fb )))))); _zm1 = y; return (in_o.proc(num) + y * _depth)*0.5; } } var AllpassDelay = function(in_o){ /* * http://www.musicdsp.org/showArchiveComment.php?ArchiveID=78 */ var _a1 = 0; var _zm1 = 0; this.Delay = function( delay ){ //sample delay time _a1 = (1 - delay) / (1 + delay); } this.proc = function( num ){ y = in_o.proc(num) * -_a1 + _zm1; _zm1 = y * _a1 + in_o.proc(num); return y; } } var ZeroCrossingWave = function(inobject){ var forder = 1; this.proc = function(num){ var temp = inobject.proc(num); var crosstemp = temp*forder; forder = temp; if (crosstemp < 0) return 1; return 0; } } /* **************test code **********************/ var mod = new SinOsc(50, 150); var adsr = new Track([1,0,0], [0.1,0,0], [1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1]); var adsr2 = new Track([300,110,0], [0.1,0,0], [1,2.1,2,1,1.8,1.9,2,1,1,1.25,3,1,2,1,2,1]); var add = new Add(mod, adsr2); var out2 = new TriOsc(0, 0, add, adsr); var out3 = new MoogVCF(out2, 4550, 0.9); var osc_adsr = new Track([300,0,0], [0.1,0,0], [1,0,0,0]); var osc_adsr2 = new Track([1,0,0], [0.1,0,0], [1,0,0,0]); var pass_adsr = new Track([3000,20,0], [0.1,0,0], [1,0,0,0]); var osc = new SqrOsc(0,0, osc_adsr,osc_adsr2); var out4 = new LowPass(osc, 0, 0.9, pass_adsr, None); var sin_adsr = new Track([200,200,0], [0.2,0,0], [1,2,3,4]); var sin_adsr2 = new Track([0.5,0.15,0], [0.2,0,0], [1,0,0,0,1,0,1,0,1,1,0,0,1,1,1,1,0,0,0,0]); var sin_adsr3 = new Track([200,300,0], [0.2,0,0], [2,2,2,2]); var string = new SawOsc(0,0,sin_adsr,sin_adsr2); var string2 = new SawOsc(0,0,sin_adsr3,sin_adsr2); var strings = new Add(string,string2); var out = new Mixer(out3,out4,strings); var sndout = new SndOut(out); </script> </head> <body> <input type="button" value='play' onclick="sndout.play()"> <input type="button" value='stop' onclick="sndout.stop()"> </body></html>