Artifact f5139aa17d77a52581d5cd4f8b05e7623358ec63209ece10ce03b388235ac21b:

  • File src/remiaudio/dsp/reverbs/mverb.cr — part of check-in [98921eb869] at 2024-01-05 07:36:37 on branch trunk — Copyright update (user: alexa size: 24872)

#### 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