CoffeeScriptでサウンドライブラリ
追記(実際にブラウザで音アプリを作りたい人はAudiolet.js がいいと思いますよ)
以前にjavascriptで書いたサウンドライブラリをCoffeeScriptで書きなおしたもの。
javascriptとcoffeescriptを相互変換するツールがあるのですがどうも望んだ感じにはならないので書いたというところ。
時間切れでfirefoxには対応していないし、奇妙なフィルタとかもなかったりする。あともう少し縮められたが気がする。
まあ名前はCoffeeSndってことで。簡易的なものであくまでネタです。
使い方。よくある感じです。
adsr = new ADSR(0, 0, 1, 0.25, 0) aseq = new Sequencer(adsr, 120, [1, 0, 1, 0, 1, 1, 1, 1]) noise = new Noise(aseq) f = new BandPass(noise, 8000, 0.9, 0, 0) out = new SndOut(f, f); out.play()
実際のライブラリ
class CoffeeSnd constructor: -> @sample_rate = 48000 @buffer_size = 2048 @timestamp = 0 @outtemp = 0 class Proxy extends CoffeeSnd constructor: (x) -> super @number = x update: (tm) -> @number note_on: -> Port = (n) -> if typeof (n) is "number" new Proxy(n) else n class Gen extends CoffeeSnd constructor: (frequency, amplitude, addfrequency, addamplitude) -> super @freq = Port(frequency) @amp = Port(amplitude) @addfreq = Port(addfrequency) @addamp = Port(addamplitude) @phase = 0 set_freq: (frequency) -> @freq = Port(frequency) set_amp: (amplitude) -> @amp = Port(amplitude) set_addfreq: (addfrequency) -> @addfreq = Port(addfrequency) set_addamp: (addamplitude) -> @addamp = Port(addamplitude) note_on: -> @phase = 0 class SinOsc extends Gen update: (tm) -> if @timestamp is tm @timestamp++ @phase += Math.abs( @freq.update(tm) + @addfreq.update(tm) )/@sample_rate @phase = (if @phase >= 1 then 0 else @phase) @outtemp = Math.sin(2*Math.PI*@phase)*@amp.update(tm) + @addamp.update(tm) else @outtemp class SqrOsc extends Gen update: (tm) -> if @timestamp is tm @timestamp++ @phase += Math.abs(@freq.update(tm) + @addfreq.update(tm))*2/@sample_rate @phase = (if @phase >= 1 then -1 else @phase) d = @amp.update(tm) @outtemp = (if @phase < 0 then d * -1 + @addamp.update(tm) else d + @addamp.update(tm)) else @outtemp class SawOsc extends Gen update: (tm) -> if @timestamp is tm @timestamp++ @phase += Math.abs( @freq.update(tm) + @addfreq.update(tm) )*2/@sample_rate @phase = (if @phase >= 1 then -1 else @phase) @outtemp = @phase*@amp.update(tm) + @addamp.update(tm) else @outtemp class TriOsc extends Gen update: (tm) -> if @timestamp is tm @timestamp++ @phase += Math.abs(@freq.update(tm) + @addfreq.update(tm)) * 2 / @sample_rate @phase = (if (@phase >= 1) then -1 else @phase) d = @phase * 2 d = (if (d > 1) then 1 - (d - 1) else d) d = (if (d < -1) then -2 - d else d) @outtemp = d * @amp.update(tm) + @addamp.update(tm) else @outtemp class TabOsc extends Gen constructor: (@wave_array, amplitude, addfrequency, addamplitude) -> super(amplitude, addfrequency, addamplitude) update: (tm) -> if @timestamp is tm @timestamp++ @phase += Math.abs(@freq.update(tm) + @addfreq.update(tm)) / @sample_rate @phase = (if (@phase >= 1) then 0 else @phase) @outtemp = @wave_array[Math.floor((@wave_array.length - 1) * @phase)] * @amp.update(tm) + @addamp.update(tm) else @outtemp class Noise extends Gen constructor: (amplitude) -> super(0, amplitude, 0, 0) update: (tm) -> Math.random() * @amp.update(tm) class ADSR extends CoffeeSnd constructor: (atc, dec, sus, sus_time, rel) -> super @start_value = new Array(4) @end_value = new Array(4) @sample_length = new Array(4) @reciprocal_sample_length = new Array(4) @current_value = 0 @count = 0 @stage = 5 @start_value[0] = @current_value @end_value[0] = 1 @sample_length[0] = atc * @sample_rate @reciprocal_sample_length[0] = (if @sample_length[0] is 0 then 0 else 1.0 / @sample_length[0]) @start_value[1] = 1 @end_value[1] = sus @sample_length[1] = dec * @sample_rate @reciprocal_sample_length[1] = (if @sample_length[1] is 0 then 0 else 1.0 / @sample_length[1]) @start_value[2] = sus @end_value[2] = sus @sample_length[2] = sus_time * @sample_rate @reciprocal_sample_length[2] = (if @sample_length[2] is 0 then 0 else 1.0 / @sample_length[2]) @start_value[3] = sus @end_value[3] = 0 @sample_length[3] = rel * @sample_rate @reciprocal_sample_length[3] = (if @sample_length[3] is 0 then 0 else 1.0 / @sample_length[3]) note_on: -> @stage = 0 @count = 0 @start_value[0] = @current_value update: (tm)-> if @timestamp is tm @timestamp++ if @stage < 4 @current_value = @start_value[@stage] + (@end_value[@stage] - @start_value[@stage]) * (@count * @reciprocal_sample_length[@stage]) if @sample_length[@stage] > @count @count = @count + 1 else @count = 0 @stage = @stage + 1 else @current_value = 0 @outtemp = @current_value else @outtemp class Sequencer extends CoffeeSnd constructor: (input, bpm, triger) -> super @input = Port(input) @bpm = (60.0 / bpm) * @sample_rate / 4.0 @triger = triger @triger_num = 0 @count = 0 @flag = true @range = 0 update: (tm) -> if @timestamp is tm @timestamp++ if @bpm < @count if @flag is true if @triger[@triger_num] @input.note_on() @range = @triger[@triger_num] @triger_num = (@triger_num + 1) % @triger.length @count = 0 @count = @count + 1 @outtemp = @input.update(tm) * @range else @outtemp class Filter extends CoffeeSnd constructor: (input, cf, q, addcf, addq) -> super @input = Port(input) @cf = Port(cf) @q = Port(q) @addcf = Port(addcf) @addq = Port(addq) @frame = 0 @b0 = 0 @b1 = 0 @b2 = 0 @a0 = 0 @a1 = 0 @a2 = 0 @forder = 0 @sorder = 0 @calc = (tm) -> update: (tm) -> if @frame > @sample_rate @calc() @frame = 0 @frame += 64 temp = (@b0 / @a0) * @input.update(tm) + (@b1 / @a0) * @forder + (@b2 / @a0) * @sorder - (@a1 / @a0) * @forder - (@a2 / @a0) * @sorder @sorder = @forder @forder = temp temp set_cf: (frequency) -> @cf = Port(cf) set_q: (amplitude) -> @q = Port(q) set_addcf: (addfrequency) -> @addcf = Port(addcf) set_addq: (addamplitude) -> @addq = Port(addq) class LowPass extends Filter constructor: (input, cf, q, addcf, addq) -> super(input, cf, q, addcf, addq) @calc = (tm) -> omega = 2 * Math.PI * Math.abs((0.5 * @cf.update(tm) + 0.5) + @addcf.update(tm)) / @sample_rate alpha = Math.sin(omega) / (2 * Math.abs((0.5 * @q.update(tm) + 0.5) + @addq.update(tm))) cs = Math.cos(omega) @b0 = (1 - cs) / 2.0 @b1 = 1 - cs @b2 = (1 - cs) / 2.0 @a0 = 1 + alpha @a1 = -2 * cs @a2 = 1 - alpha @calc(0) class HighPass extends Filter constructor: (input, cf, q, addcf, addq) -> super(input, cf, q, addcf, addq) @calc = (tm) -> omega = 2 * Math.PI * Math.abs((0.5 * @cf.update(tm) + 0.5) + @addcf.update(tm)) / @sample_rate alpha = Math.sin(omega) / (2 * Math.abs((0.5 * @q.update(tm) + 0.5) + @addq.update(tm))) cs = Math.cos(omega) @b0 = (1 + cs) / 2 @b1 = -(1 + cs) @b2 = (1 + cs) / 2 @a0 = 1 + alpha @a1 = -2 * cs @a2 = 1 - alpha @calc(0) class BandPass extends Filter constructor: (input, cf, q, addcf, addq) -> super(input, cf, q, addcf, addq) @calc = (tm) -> sinh = (arg) -> (Math.exp(arg) - Math.exp(-arg)) / 2 omega = 2 * Math.PI * Math.abs((0.5 * @cf.update(tm) + 0.5) + @addcf.update(tm)) / @sample_rate alpha = Math.sin(omega) * sinh(Math.log(2) / 2 * Math.abs(@q.update(tm) + @addq.update(tm)) * omega / Math.sin(omega)) cs = Math.cos(omega) @b0 = alpha @b1 = 0 @b2 = -alpha @a0 = 1 + alpha @a1 = -2 * cs @a2 = 1 - alpha @calc(0) class Operation constructor: (a, b) -> @a = Port(a) @b = Port(b) set_A: (a) -> @a = Port(a) set_B: (b) -> @b = Port(b) class Add extends Operation update: (tm) -> @a.update(tm) + @b.update(tm) class Mult extends Operation update: (tm) -> @a.update(tm) * @b.update(tm) class SndOut extends CoffeeSnd constructor: (input, input2) -> super @audiocontext = new webkitAudioContext(); @audiocontext.sampleRate = @sample_rate; @node = @audiocontext.createJavaScriptNode(@buffer_size, 0, 2); timestamp = 0 @node.onaudioprocess = (event) -> data = event.outputBuffer.getChannelData(0); data2 = event.outputBuffer.getChannelData(1); i = 0 while i < data.length data[i] = input.update(timestamp) data2[i] = input2.update(timestamp) i++ timestamp++ play: -> @node.connect(@audiocontext.destination) stop: -> @node.disconnect()