CoffeeScriptでサウンドライブラリ

追記(実際にブラウザで音アプリを作りたい人はAudiolet.js がいいと思いますよ)

以前にjavascriptで書いたサウンドライブラリをCoffeeScriptで書きなおしたもの。
javascriptcoffeescriptを相互変換するツールがあるのですがどうも望んだ感じにはならないので書いたというところ。

時間切れで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()