#### 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"
require "./mverb-classes"
require "./mverb-presets"
module RemiAudio::DSP
# Implements a reverb effect.
#
# This is a port of MVerb, a studio quality, open-source reverb based on a
# figure-of-eight structure.
class MVerb < Reverb
DEFAULT_DAMPING_FREQ = 0.5
DAMPING_FREQ_MIN = 0.0
DAMPING_FREQ_MAX = 1.0
DEFAULT_DENSITY = 0.5
DENSITY_MIN = 0.0
DENSITY_MAX = 1.0
DEFAULT_BANDWIDTH_FREQ = 0.5
BANDWIDTH_FREQ_MIN = 0.0
BANDWIDTH_FREQ_MAX = 1.0
DEFAULT_DECAY = 0.5
DECAY_MIN = 0.0
DECAY_MAX = 1.0
DEFAULT_PREDELAY = 0.5
PREDELAY_MIN = 0.0
PREDELAY_MAX = 1.0
DEFAULT_SIZE = 0.5
SIZE_MIN = 0.0
SIZE_MAX = 1.0
DEFAULT_GAIN = 1.0
GAIN_MIN = 0.0
GAIN_MAX = 1.0
DEFAULT_MIX = 0.5
MIX_MIN = 0.0
MIX_MAX = 1.0
DEFAULT_EARLY_LATE_MIX = 0.5
EARLY_LATE_MIX_MIN = 0.0
EARLY_LATE_MIX_MAX = 1.0
getter sampleRate : Float64 = 44100.0
getter dampingFreq : Float64 = 0.9
@density1 : Float64 = 0.0
@density2 : Float64 = 0.0
getter bandwidthFreq : Float64 = 0.9
@predelayTime : Float64 = 0.0
getter decay : Float64 = 0.5
getter gain : Float64 = 1.0
getter mix : Float64 = 1.0
getter earlyLateMix : Float64 = 1.0
getter size : Float64 = 1.0
@mixSmooth : Float64 = 0.0
@earlyLateSmooth : Float64 = 0.0
@bandwidthSmooth : Float64 = 0.0
@dampingSmooth : Float64 = 0.0
@predelaySmooth : Float64 = 0.0
@sizeSmooth : Float64 = 0.0
@densitySmooth : Float64 = 0.0
@decaySmooth : Float64 = 0.0
@previousLeftTank : Float64 = 0.0
@previousRightTank : Float64 = 0.0
@controlRate : Int32 = 441
@controlRateCounter : Int32 = 0
@allpass : Array(Allpass) =
Array(Allpass).new(4) { |_| RemiAudio::DSP::MVerb::Allpass.new(96000) }
@allpassFourTap : Array(StaticAllpassFourTap) =
Array(StaticAllpassFourTap).new(4) { |_| RemiAudio::DSP::MVerb::StaticAllpassFourTap.new(96000) }
@bandwidthFilter : Array(StateVariable) =
Array(StateVariable).new(2) { |_| RemiAudio::DSP::MVerb::StateVariable.new(4) }
@damping : Array(StateVariable) =
Array(StateVariable).new(2) { |_| RemiAudio::DSP::MVerb::StateVariable.new(4) }
@staticDelayLine : Array(StaticDelayLineFourTap) =
Array(StaticDelayLineFourTap).new(4) { |_| RemiAudio::DSP::MVerb::StaticDelayLineFourTap.new(96000) }
@earlyReflectionsDelayLine : Array(StaticDelayLineEightTap) =
Array(StaticDelayLineEightTap).new(2) { |_| RemiAudio::DSP::MVerb::StaticDelayLineEightTap.new(96000) }
@predelay : StaticDelayLine = StaticDelayLine.new(96000)
def initialize(newSampleRate : Number)
@sampleRate = newSampleRate.to_f64
@predelayTime = 100.0 * (@sampleRate / 1000.0)
@controlRate = (@sampleRate / 1000.0).to_i32!
reset
end
# Purposely unhygenic. Sets up various variables for the process() methods.
private macro processCommon(bufferSize)
invSampleFrames : Float64 = 1.0 / ({{bufferSize}})
mixDelta : Float64 = (@mix - @mixSmooth) * invSampleFrames
earlyLateDelta : Float64 = (@earlyLateMix - @earlyLateSmooth) * invSampleFrames
bandwidthDelta : Float64 = (((@bandwidthFreq * 18400.0) + 100.0) - @bandwidthSmooth) * invSampleFrames
dampingDelta : Float64 = (((@dampingFreq * 18400.0) + 100.0) - @dampingSmooth) * invSampleFrames
predelayDelta : Float64 = ((@predelayTime * 200.0 * (@sampleRate / 1000.0)) - @predelaySmooth) * invSampleFrames
sizeDelta : Float64 = (@size - @sizeSmooth) * invSampleFrames
decayDelta : Float64 = (((0.7995 * @decay) + 0.005) - @decaySmooth) * invSampleFrames
densityDelta : Float64 = (((0.7995 * @density1) + 0.005) - @densitySmooth) * invSampleFrames
bandwidthLeft : Float64 = 0.0
bandwidthRight : Float64 = 0.0
erLeft : Float64 = 0.0
erRight : Float64 = 0.0
predelayMonoInput : Float64 = 0.0
smearedInput : Float64 = 0.0
leftTank : Float64 = 0.0
rightTank : Float64 = 0.0
accLeft : Float64 = 0.0
accRight : Float64 = 0.0
end
# Does the actual processing. `left` and `right` are overridden here.
private macro processLeftRight(left, right, outLeft, outRight)
@mixSmooth += mixDelta
@earlyLateSmooth += earlyLateDelta
@bandwidthSmooth += bandwidthDelta
@dampingSmooth += dampingDelta
@predelaySmooth += predelayDelta
@sizeSmooth += sizeDelta
@decaySmooth += decayDelta
@densitySmooth += densityDelta
if @controlRateCounter >= @controlRate
@controlRateCounter = 0
@bandwidthFilter.unsafe_fetch(0).frequency = @bandwidthSmooth
@bandwidthFilter.unsafe_fetch(1).frequency = @bandwidthSmooth
@damping.unsafe_fetch(0).frequency = @dampingSmooth
@damping.unsafe_fetch(1).frequency = @dampingSmooth
end
@controlRateCounter += 1
@predelay.length = @predelaySmooth.to_i32!
@density2 = (@decaySmooth + 0.15).clamp(0.25, 0.5)
@allpassFourTap.unsafe_fetch(1).feedback = @density2
@allpassFourTap.unsafe_fetch(3).feedback = @density2
@allpassFourTap.unsafe_fetch(0).feedback = @density1
@allpassFourTap.unsafe_fetch(2).feedback = @density1
bandwidthLeft = @bandwidthFilter.unsafe_fetch(0).process({{left}})
bandwidthRight = @bandwidthFilter.unsafe_fetch(1).process({{right}})
erLeft = @earlyReflectionsDelayLine.unsafe_fetch(0).process(bandwidthLeft * 0.5 + bandwidthRight * 0.3) +
@earlyReflectionsDelayLine.unsafe_fetch(0).idx2 * 0.6 +
@earlyReflectionsDelayLine.unsafe_fetch(0).idx3 * 0.4 +
@earlyReflectionsDelayLine.unsafe_fetch(0).idx4 * 0.3 +
@earlyReflectionsDelayLine.unsafe_fetch(0).idx5 * 0.3 +
@earlyReflectionsDelayLine.unsafe_fetch(0).idx6 * 0.1 +
@earlyReflectionsDelayLine.unsafe_fetch(0).idx7 * 0.1 +
(bandwidthLeft * 0.4 + bandwidthRight * 0.2) * 0.5
erRight = @earlyReflectionsDelayLine.unsafe_fetch(1).process(bandwidthLeft * 0.3 + bandwidthRight * 0.5) +
@earlyReflectionsDelayLine.unsafe_fetch(1).idx2 * 0.6 +
@earlyReflectionsDelayLine.unsafe_fetch(1).idx3 * 0.4 +
@earlyReflectionsDelayLine.unsafe_fetch(1).idx4 * 0.3 +
@earlyReflectionsDelayLine.unsafe_fetch(1).idx5 * 0.3 +
@earlyReflectionsDelayLine.unsafe_fetch(1).idx6 * 0.1 +
@earlyReflectionsDelayLine.unsafe_fetch(1).idx7 * 0.1 +
(bandwidthLeft * 0.2 + bandwidthRight * 0.4) * 0.5
predelayMonoInput = @predelay.process((bandwidthRight + bandwidthLeft) * 0.5)
smearedInput = predelayMonoInput
4.times { |j| smearedInput = @allpass.unsafe_fetch(j).process(smearedInput) }
leftTank = @allpassFourTap.unsafe_fetch(0).process(smearedInput + @previousRightTank)
leftTank = @staticDelayLine.unsafe_fetch(0).process(leftTank)
leftTank = @damping.unsafe_fetch(0).process(leftTank)
leftTank = @allpassFourTap.unsafe_fetch(1).process(leftTank)
leftTank = @staticDelayLine.unsafe_fetch(1).process(leftTank)
rightTank = @allpassFourTap.unsafe_fetch(2).process(smearedInput + @previousLeftTank)
rightTank = @staticDelayLine.unsafe_fetch(2).process(rightTank)
rightTank = @damping.unsafe_fetch(1).process(rightTank)
rightTank = @allpassFourTap.unsafe_fetch(3).process(rightTank)
rightTank = @staticDelayLine.unsafe_fetch(3).process(rightTank)
@previousLeftTank = leftTank * @decaySmooth
@previousRightTank = rightTank * @decaySmooth
accLeft = (0.6 * @staticDelayLine.unsafe_fetch(2).idx1) +
(0.6 * @staticDelayLine.unsafe_fetch(2).idx2) -
(0.6 * @allpassFourTap.unsafe_fetch(3).idx1) +
(0.6 * @staticDelayLine.unsafe_fetch(3).idx1) -
(0.6 * @staticDelayLine.unsafe_fetch(0).idx1) -
(0.6 * @allpassFourTap.unsafe_fetch(1).idx1) -
(0.6 * @staticDelayLine.unsafe_fetch(1).idx1)
accRight = (0.6 * @staticDelayLine.unsafe_fetch(0).idx2) +
(0.6 * @staticDelayLine.unsafe_fetch(0).idx3) -
(0.6 * @allpassFourTap.unsafe_fetch(1).idx2) +
(0.6 * @staticDelayLine.unsafe_fetch(1).idx2) -
(0.6 * @staticDelayLine.unsafe_fetch(2).idx3) -
(0.6 * @allpassFourTap.unsafe_fetch(3).idx2) -
(0.6 * @staticDelayLine.unsafe_fetch(3).idx2)
accLeft = (accLeft * @earlyLateMix) + ((1.0 - @earlyLateMix) * erLeft)
accRight = (accRight * @earlyLateMix) + ((1.0 - @earlyLateMix) * erRight)
{{outLeft}} = ({{left}} + @mixSmooth * (accLeft - {{left}})) * @gain
{{outRight}} = ({{right}} + @mixSmooth * (accRight - {{right}})) * @gain
end
# Applies a reverb effect to `inputLeft` and `inputRight`, storing the 100%
# wet results in `outputLeft` and `outputRight`. The size of both inputs
# and both outputs must all match.
def process(inputLeft : Array(Float64)|Slice(Float64), inputRight : Array(Float64)|Slice(Float64),
outputLeft : Array(Float64)|Slice(Float64), outputRight : Array(Float64)|Slice(Float64)) : Nil
processCommon(inputLeft.size)
inputLeft.size.times do |i|
processLeftRight(inputLeft[i], inputRight[i], outputLeft[i], outputRight[i])
end
end
# :ditto:
def process(inputLeft : Array(Float32)|Slice(Float32), inputRight : Array(Float32)|Slice(Float32),
outputLeft : Array(Float32)|Slice(Float32), outputRight : Array(Float32)|Slice(Float32)) : Nil
outLeft : Float64 = 0.0
outRight : Float64 = 0.0
processCommon(inputLeft.size)
inputLeft.size.times do |i|
processLeftRight(inputLeft[i].to_f64!, inputRight[i].to_f64!, outLeft, outRight)
outputLeft[i] = outLeft.to_f32!
outputRight[i] = outRight.to_f32!
end
end
# Applies a reverb effect to the interleaved samples in `input`, storing the
# 100% wet results as interleaved samples in `output`. The size of both the
# input and output buffers must match.
def process(input : Array(Float64)|Slice(Float64), output : Array(Float64)|Slice(Float64)) : Nil
processCommon(input.size // 2)
left : Float64 = 0.0
right : Float64 = 0.0
outLeft : Float64 = 0.0
outRight : Float64 = 0.0
i : Int32 = 0
while i < input.size
left = input[i]
right = input[i + 1]
processLeftRight(left, right, outLeft, outRight)
output[i] = outLeft
output[i + 1] = outRight
i += 2
end
end
# :ditto:
def process(input : Array(Float32)|Slice(Float32), output : Array(Float32)|Slice(Float32)) : Nil
processCommon(input.size // 2)
i : Int32 = 0
left : Float64 = 0.0
right : Float64 = 0.0
outLeft : Float64 = 0.0
outRight : Float64 = 0.0
while i < input.size
left = input[i].to_f64!
right = input[i + 1].to_f64!
processLeftRight(left, right, outLeft, outRight)
output[i] = outLeft.to_f32!
output[i + 1] = outRight.to_f32!
i += 2
end
end
# Applies a reverb effect to the interleaved samples in `buffer`, mixing the
# the results back into `buffer`. `multiplier` dictates how strong the
# effect is, and should ideally be between 0.0 and 1.0, inclusive.
def process(buffer : Array(Float32)|Slice(Float32), multiplier : Float32 = 1.0) : Nil
processCommon(buffer.size // 2)
i : Int32 = 0
left : Float64 = 0.0
right : Float64 = 0.0
outLeft : Float64 = 0.0
outRight : Float64 = 0.0
while i < buffer.size
left = buffer[i].to_f64!
right = buffer[i + 1].to_f64!
processLeftRight(left, right, outLeft, outRight)
buffer[i] += (multiplier * outLeft).to_f32!
buffer[i + 1] += (multiplier * outRight).to_f32!
i += 2
end
end
# :ditto:
def process(buffer : Array(Float64)|Slice(Float64), multiplier : Float64 = 1.0) : Nil
processCommon(buffer.size // 2)
i : Int32 = 0
left : Float64 = 0.0
right : Float64 = 0.0
outLeft : Float64 = 0.0
outRight : Float64 = 0.0
while i < buffer.size
left = buffer[i]
right = buffer[i + 1]
processLeftRight(left, right, outLeft, outRight)
buffer[i] += multiplier * outLeft
buffer[i + 1] += multiplier * outRight
i += 2
end
end
def reset : Nil
@controlRateCounter = 0
@bandwidthFilter[0].sampleRate = @sampleRate
@bandwidthFilter[0].reset
@bandwidthFilter[1].sampleRate = @sampleRate
@bandwidthFilter[1].reset
@damping[0].sampleRate = @sampleRate
@damping[0].reset
@damping[1].sampleRate = @sampleRate
@damping[1].reset
@predelay.clear
@predelay.length = @predelayTime.to_i32!
4.times { |i| @allpass[i].clear }
@allpass[0].length = (0.0048 * @sampleRate).to_i32!
@allpass[1].length = (0.0036 * @sampleRate).to_i32!
@allpass[2].length = (0.0127 * @sampleRate).to_i32!
@allpass[3].length = (0.0093 * @sampleRate).to_i32!
@allpass[0].feedback = 0.75
@allpass[1].feedback = 0.75
@allpass[2].feedback = 0.625
@allpass[3].feedback = 0.625
4.times { |i| @allpassFourTap[i].clear }
@allpassFourTap[0].length = (0.020 * @sampleRate * @size).to_i32!
@allpassFourTap[1].length = (0.060 * @sampleRate * @size).to_i32!
@allpassFourTap[2].length = (0.030 * @sampleRate * @size).to_i32!
@allpassFourTap[3].length = (0.089 * @sampleRate * @size).to_i32!
@allpassFourTap[0].feedback = @density1
@allpassFourTap[1].feedback = @density2
@allpassFourTap[2].feedback = @density1
@allpassFourTap[3].feedback = @density2
@allpassFourTap[0].setIndex(0, 0, 0, 0)
@allpassFourTap[1].setIndex(0, (0.006 * @sampleRate * @size).to_i32!, (0.041 * @sampleRate * @size).to_i32!, 0)
@allpassFourTap[2].setIndex(0, 0, 0, 0)
@allpassFourTap[3].setIndex(0, (0.031 * @sampleRate * @size).to_i32!, (0.011 * @sampleRate * @size).to_i32!, 0)
4.times { |i| @staticDelayLine[i].clear }
@staticDelayLine[0].length = (0.15 * @sampleRate * @size).to_i32!
@staticDelayLine[1].length = (0.12 * @sampleRate * @size).to_i32!
@staticDelayLine[2].length = (0.14 * @sampleRate * @size).to_i32!
@staticDelayLine[3].length = (0.11 * @sampleRate * @size).to_i32!
@staticDelayLine[0].setIndex(0, (0.067 * @sampleRate * @size).to_i32!, (0.011 * @sampleRate * @size).to_i32!,
(0.121 * @sampleRate * @size).to_i32!)
@staticDelayLine[1].setIndex(0, (0.036 * @sampleRate * @size).to_i32!, (0.089 * @sampleRate * @size).to_i32!, 0)
@staticDelayLine[2].setIndex(0, (0.0089 * @sampleRate * @size).to_i32!, (0.099 * @sampleRate * @size).to_i32!, 0)
@staticDelayLine[3].setIndex(0, (0.067 * @sampleRate * @size).to_i32!, (0.0041 * @sampleRate * @size).to_i32!, 0)
2.times { |i| @earlyReflectionsDelayLine[i].clear }
@earlyReflectionsDelayLine[0].length = (0.089 * @sampleRate).to_i32!
@earlyReflectionsDelayLine[0].setIndex(0,
(0.0199 * @sampleRate).to_i32!,
(0.0219 * @sampleRate).to_i32!,
(0.0354 * @sampleRate).to_i32!,
(0.0389 * @sampleRate).to_i32!,
(0.0414 * @sampleRate).to_i32!,
(0.0692 * @sampleRate).to_i32!,
0)
@earlyReflectionsDelayLine[1].length = (0.069 * @sampleRate).to_i32!
@earlyReflectionsDelayLine[1].setIndex(0,
(0.0099 * @sampleRate).to_i32!,
(0.011 * @sampleRate).to_i32!,
(0.0182 * @sampleRate).to_i32!,
(0.0189 * @sampleRate).to_i32!,
(0.0213 * @sampleRate).to_i32!,
(0.0431 * @sampleRate).to_i32!,
0)
end
def mute : Nil
@predelay.clear
@allpass.each &.clear
@allpassFourTap.each &.clear
@staticDelayLine.each &.clear
@earlyReflectionsDelayLine.each &.clear
@bandwidthFilter.each &.reset
@damping.each &.reset
@previousLeftTank = 0.0
@previousRightTank = 0.0
@mixSmooth = 0.0
@earlyLateSmooth = 0.0
@bandwidthSmooth = 0.0
@dampingSmooth = 0.0
@predelaySmooth = 0.0
@sizeSmooth = 0.0
@decaySmooth = 0.0
@densitySmooth = 0.0
@controlRateCounter = 0
end
def sampleRate=(value : Number) : Float64
raise "Invalid sample rate" if value < 1
@sampleRate = value.to_f64
@predelayTime = 100.0 * (@sampleRate / 1000.0)
@controlRate = (@sampleRate / 1000.0).to_i32!
reset
@sampleRate
end
def bandwidthFreq=(value : Float64) : Float64
MVerb.checkBandwidthFreq(value)
@bandwidthFreq = value
end
def decay=(value : Float64) : Float64
MVerb.checkDecay(value)
@decay = value
end
def gain=(value : Float64) : Float64
MVerb.checkGain(value)
@gain = value
end
def mix=(value : Float64) : Float64
MVerb.checkMix(value)
@mix = value
end
def earlyLateMix=(value : Float64) : Float64
MVerb.checkEarlyLateMix(value)
@earlyLateMix = value
end
def dampingFreq=(value : Float64) : Float64
MVerb.checkDampingFreq(value)
@dampingFreq = 1.0 - value
end
def density : Float64
@density1
end
def density=(value : Float64) : Float64
MVerb.checkDensity(value)
@density1 = value
end
def predelay : Float64
@predelayTime
end
def predelay=(value : Float64) : Float64
MVerb.checkPredelay(value)
@predelayTime = value
end
def size=(value : Float64) : Float64
MVerb.checkSize(value)
@size = (0.95 * value) + 0.05
4.times { |i| @allpassFourTap[i].clear }
@allpassFourTap[0].length = (0.020 * @sampleRate * @size).to_i32!
@allpassFourTap[1].length = (0.060 * @sampleRate * @size).to_i32!
@allpassFourTap[2].length = (0.030 * @sampleRate * @size).to_i32!
@allpassFourTap[3].length = (0.089 * @sampleRate * @size).to_i32!
@allpassFourTap[1].setIndex(0, (0.006 * @sampleRate * @size).to_i32!, (0.041 * @sampleRate * @size).to_i32!, 0)
@allpassFourTap[3].setIndex(0, (0.031 * @sampleRate * @size).to_i32!, (0.011 * @sampleRate * @size).to_i32!, 0)
4.times { |i| @staticDelayLine[i].clear }
@staticDelayLine[0].length = (0.15 * @sampleRate * @size).to_i32!
@staticDelayLine[1].length = (0.12 * @sampleRate * @size).to_i32!
@staticDelayLine[2].length = (0.14 * @sampleRate * @size).to_i32!
@staticDelayLine[3].length = (0.11 * @sampleRate * @size).to_i32!
@staticDelayLine[0].setIndex(0, (0.067 * @sampleRate * @size).to_i32!, (0.011 * @sampleRate * @size).to_i32!,
(0.121 * @sampleRate * @size).to_i32!)
@staticDelayLine[1].setIndex(0, (0.036 * @sampleRate * @size).to_i32!, (0.089 * @sampleRate * @size).to_i32!, 0)
@staticDelayLine[2].setIndex(0, (0.0089 * @sampleRate * @size).to_i32!, (0.099 * @sampleRate * @size).to_i32!, 0)
@staticDelayLine[3].setIndex(0, (0.067 * @sampleRate * @size).to_i32!, (0.0041 * @sampleRate * @size).to_i32!, 0)
@size
end
def usePreset(preset : Reverb::Preset) : Nil
if data = preset.as?(MVerb::Preset)
self.dampingFreq = data.dampingFreq
self.density = data.density
self.bandwidthFreq = data.bandwidthFreq
self.decay = data.decay
self.predelay = data.predelay
self.size = data.size
self.gain = data.gain
self.mix = data.mix
self.earlyLateMix = data.earlyLateMix
else
raise "Bad preset type for MVerb: #{preset.class}"
end
end
# Checks if `value` is a valid damping value. If it is, this does nothing.
# Otherwise this raises a `ReverbError` with an explanation.
@[AlwaysInline]
def self.checkDampingFreq(value) : Nil
unless value >= DAMPING_FREQ_MIN && value <= DAMPING_FREQ_MAX
raise ReverbError.new("Damping value for MVerb must be between #{DAMPING_FREQ_MIN} and #{DAMPING_FREQ_MAX}")
end
end
# Checks if `value` is a valid density value. If it is, this does nothing.
# Otherwise this raises a `ReverbError` with an explanation.
@[AlwaysInline]
def self.checkDensity(value) : Nil
unless value >= DENSITY_MIN && value <= DENSITY_MAX
raise ReverbError.new("Density value for MVerb must be between #{DENSITY_MIN} and #{DENSITY_MAX}")
end
end
# Checks if `value` is a valid bandwidth frequency value. If it is, this
# does nothing. Otherwise this raises a `ReverbError` with an explanation.
@[AlwaysInline]
def self.checkBandwidthFreq(value) : Nil
unless value >= BANDWIDTH_FREQ_MIN && value <= BANDWIDTH_FREQ_MAX
raise ReverbError.new("Bandwidth frequency value for MVerb must be between #{BANDWIDTH_FREQ_MIN} and #{BANDWIDTH_FREQ_MAX}")
end
end
# Checks if `value` is a valid decay value. If it is, this does nothing.
# Otherwise this raises a `ReverbError` with an explanation.
@[AlwaysInline]
def self.checkDecay(value) : Nil
unless value >= DECAY_MIN && value <= DECAY_MAX
raise ReverbError.new("Decay value for MVerb must be between #{DECAY_MIN} and #{DECAY_MAX}")
end
end
# Checks if `value` is a valid predelay value. If it is, this does nothing.
# Otherwise this raises a `ReverbError` with an explanation.
@[AlwaysInline]
def self.checkPredelay(value) : Nil
unless value >= PREDELAY_MIN && value <= PREDELAY_MAX
raise ReverbError.new("Pre-delay value for MVerb must be between #{PREDELAY_MIN} and #{PREDELAY_MAX}")
end
end
# Checks if `value` is a valid size value. If it is, this does nothing.
# Otherwise this raises a `ReverbError` with an explanation.
@[AlwaysInline]
def self.checkSize(value) : Nil
unless value >= SIZE_MIN && value <= SIZE_MAX
raise ReverbError.new("Size value for MVerb must be between #{SIZE_MIN} and #{SIZE_MAX}")
end
end
# Checks if `value` is a valid gain value. If it is, this does nothing.
# Otherwise this raises a `ReverbError` with an explanation.
@[AlwaysInline]
def self.checkGain(value) : Nil
unless value >= GAIN_MIN && value <= GAIN_MAX
raise ReverbError.new("Gain value for MVerb must be between #{GAIN_MIN} and #{GAIN_MAX}")
end
end
# Checks if `value` is a valid mix value. If it is, this does nothing.
# Otherwise this raises a `ReverbError` with an explanation.
@[AlwaysInline]
def self.checkMix(value) : Nil
unless value >= MIX_MIN && value <= MIX_MAX
raise ReverbError.new("Mix value for MVerb must be between #{MIX_MIN} and #{MIX_MAX}")
end
end
# Checks if `value` is a valid early/late mix value. If it is, this does
# nothing. Otherwise this raises a `ReverbError` with an explanation.
@[AlwaysInline]
def self.checkEarlyLateMix(value) : Nil
unless value >= EARLY_LATE_MIX_MIN && value <= EARLY_LATE_MIX_MAX
raise ReverbError.new("Early/Late Mix value for MVerb must be between #{EARLY_LATE_MIX_MIN} and #{EARLY_LATE_MIX_MAX}")
end
end
end
end