#### RemiAudio
#### Copyright (C) 2022-2024 Remilia Scarlet <remilia@posteo.jp>
#### Based on MVerb
#### Copyright (c) 2010 Martin Eastwood
####
#### This program is free software: you can redistribute it and/or modify it
#### under the terms of the GNU Affero General Public License as published by
#### the Free Software Foundation, either version 3 of the License, or (at your
#### option) any later version.
####
#### This program is distributed in the hope that it will be useful, but WITHOUT
#### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
#### FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
#### License for more details.
####
#### You should have received a copy of the GNU Affero General Public License
#### along with this program. If not, see <https://www.gnu.org/licenses/>.
require "./reverb"
module RemiAudio::DSP
class MVerb < Reverb
# :nodoc:
class StateVariable
enum FilterType
Lowpass
Highpass
Bandpass
Notch
end
@oversampleCount : Int32 = 1
@sampleRate : Float64 = 0.0
@frequency : Float64 = 0.0
@q : Float64 = 0.0
@f : Float64 = 0.0
@low : Float64 = 0.0
@high : Float64 = 0.0
@band : Float64 = 0.0
@notch : Float64 = 0.0
property type : FilterType = FilterType::Lowpass
def initialize(@oversampleCount : Int32)
self.sampleRate = 44100.0
self.frequency = 1000.0
self.resonance = 0.0
self.type = FilterType::Lowpass
self.reset
end
@[AlwaysInline]
def process(input : Float64) : Float64
@oversampleCount.times do |_|
@low += @f * @band + 1.0e-25
@high = input - @low - @q * @band
@band += @f * @high
@notch = @low + @high
end
case @type
in .lowpass? then @low
in .highpass? then @high
in .bandpass? then @band
in .notch? then @notch
end
end
def reset
@low = 0.0
@high = 0.0
@band = 0.0
@notch = 0.0
end
def sampleRate=(value : Float64) : Float64
@sampleRate = value * @oversampleCount
updateCoefficients
@sampleRate
end
def frequency=(value : Float64) : Float64
@frequency = value
updateCoefficients
@frequency
end
def resonance=(value : Float64) : Float64
@q = 2.0 - 2.0 * value
end
@[AlwaysInline]
private def updateCoefficients : Nil
@f = 2.0 * Math.sin(Math::PI * @frequency / @sampleRate)
end
end
private macro defineDelayLine(name, ntaps)
# :nodoc:
class {{name.id}}
@maxLength : Int32 = 1
@buffer : Array(Float64)
getter length : Int32 = 0
{% for i in (0...ntaps) %}
@index{{i.id}} : Int32 = 0
{% end %}
def initialize(@maxLength : Int32)
@buffer = Array(Float64).new(@maxLength, 0.0)
self.length = @maxLength - 1
clear
end
@[AlwaysInline]
def process(input : Float64) : Float64
ret : Float64 = @buffer.unsafe_fetch(@index0)
@buffer.unsafe_put(@index0, input)
{% for i in (0...ntaps) %}
@index{{i.id}} = 0 if (@index{{i.id}} += 1) >= @length
{% end %}
ret
end
@[AlwaysInline]
def length=(len : Int32) : Int32
@length = len.clamp(0, @maxLength)
end
def clear : Nil
@buffer.fill(0.0)
{% for i in (0...ntaps) %}
@index{{i.id}} = 0
{% end %}
end
{% unless ntaps == 1 %}
@[AlwaysInline]
def setIndex(
{% for i in (0...ntaps) %}
@index{{i.id}} : Int32 {% unless i == ntaps - 1 %},{% end %}
{% end %}
) : Nil
end
{% end %}
{% for i in 0...ntaps %}
@[AlwaysInline]
def idx{{i.id}} : Float64
@buffer.unsafe_fetch(@index{{i.id}})
end
{% end %}
end
end
# :nodoc:
class StaticAllpassFourTap
@maxLength : Int32 = 1
@buffer : Array(Float64)
@index0 : Int32 = 0
@index1 : Int32 = 0
@index2 : Int32 = 0
@index3 : Int32 = 0
property feedback : Float64 = 0.5
getter length : Int32 = 0
def initialize(@maxLength : Int32)
@buffer = Array(Float64).new(@maxLength, 0.0)
self.length = @maxLength - 1
clear
end
@[AlwaysInline]
def process(input : Float64) : Float64
bufout : Float64 = @buffer[@index0]
tmp : Float64 = input * -@feedback
ret : Float64 = bufout + tmp
@buffer.unsafe_put(@index0, input + ((bufout + tmp) * @feedback))
@index0 = 0 if (@index0 += 1) >= @length
@index1 = 0 if (@index1 += 1) >= @length
@index2 = 0 if (@index2 += 1) >= @length
@index3 = 0 if (@index3 += 1) >= @length
ret
end
@[AlwaysInline]
def setIndex(@index0 : Int32, @index1 : Int32, @index2 : Int32, @index3 : Int32) : Nil
end
{% begin %}
{% for i in 0..3 %}
@[AlwaysInline]
def idx{{i.id}} : Float64
@buffer.unsafe_fetch(@index{{i.id}})
end
{% end %}
{% end %}
@[AlwaysInline]
def length=(value : Int32) : Int32
@length = value.clamp(0, @maxLength)
end
def clear : Nil
@buffer.fill(0.0)
@index0 = 0
@index1 = 0
@index2 = 0
@index3 = 0
end
end
# :nodoc:
class Allpass
@maxLength : Int32 = 1
@buffer : Array(Float64)
@index : Int32 = 0
property feedback : Float64 = 0.5
getter length : Int32 = 0
def initialize(@maxLength : Int32)
@buffer = Array(Float64).new(@maxLength, 0.0)
self.length = @maxLength - 1
clear
end
@[AlwaysInline]
def process(input : Float64) : Float64
bufout : Float64 = @buffer.unsafe_fetch(@index)
tmp : Float64 = input * -@feedback
ret : Float64 = bufout + tmp
@buffer.unsafe_put(@index, input + ((bufout + tmp) * @feedback))
@index = 0 if (@index += 1) >= @length
ret
end
@[AlwaysInline]
def length=(value : Int32) : Int32
@length = value.clamp(0, @maxLength)
end
@[AlwaysInline]
def clear : Nil
@buffer.fill(0.0)
@index = 0
end
end
defineDelayLine(StaticDelayLine, 1)
defineDelayLine(StaticDelayLineFourTap, 4)
defineDelayLine(StaticDelayLineEightTap, 8)
end
end