Artifact c65eb27bde0a5a449a482f5acad2608a5381444a39dc9f81a83af4c6d600134f:

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

#### RemiAudio
#### Copyright (C) 2022-2024 Remilia Scarlet <remilia@posteo.jp>
####   Based on code from TAL-NoiseMaker
####   Copyright(c) 2005-2010 Patrick Kunz, TAL Togu Audio Line, Inc.
####
#### 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 "../common"
require "../interpolation"
require "./dcfilter"

####
#### This code is based on Tal-NoiseMaker.
#### http://kunz.corrupt.ch
####

module RemiAudio::DSP
  # Implements a chorus effect that is reminiscent of a certain synthesizer from
  # the 80s with a model number ending in "-60".
  class HeraChorus

    # The `HeraChorus` class actually uses two separate chorus units in series
    # when generating the effect.  The `HeraElement` class is a single one of
    # these chorus units.
    private class HeraElement
      @delayLine : Array(Float64)
      @writePos : Int32
      @rate : Float64
      @sampleRate : Int32
      @delayTime : Float64
      @offset : Float64 = 0.0
      @lastFilter : Float64 = 0.0
      @sign : Float64 = 0.0
      @lfoPhase : Float64
      @lfoStepSize : Float64
      @lfoSign : Float64 = 1.0
      property interpolationMode : Interpolate::Mode = Interpolate::Mode::Cubic

      def initialize(@sampleRate : Int32, @phase : Float64, @rate : Float64, @delayTime : Float64)
        tableLen = 2 * (@delayTime * @sampleRate * 0.001).floor.to_i32!
        @lfoPhase = @phase * 2.0 - 1.0
        @lfoStepSize = (4.0 * @rate) / @sampleRate
        @delayLine = Array(Float64).new(tableLen, 0.0)
        @writePos = tableLen - 2
      end

      @[AlwaysInline]
      protected def mute : Nil
        @delayLine.fill(0.0f64)
      end

      @[AlwaysInline]
      private def nextLfo : Float64
        case
        when @lfoPhase >= 1.0 then @lfoSign = -1.0
        when @lfoPhase <= -1.0 then @lfoSign = 1.0
        end
        @lfoPhase += @lfoStepSize * @lfoSign
      end

      protected def process(sample : Float64) : Float64
        bufferLen = @delayLine.size
        offset : Float64 = (nextLfo * 0.3 + 0.4) * @delayTime * @sampleRate * 0.001
        pos : Float64 = @writePos - offset
        pos += bufferLen if pos < 0

        index0 : Int32 = pos.to_i32! - 1
        index1 : Int32 = index0 + 1
        index2 : Int32 = index1 + 1
        index3 : Int32 = index2 + 1

        index0 += bufferLen if index0 < 0
        if index2 >= bufferLen
          index2 = 0
          index3 = 1
        end
        index3 = 0 if index3 >= bufferLen

        x0 : Float64 = @delayLine.unsafe_fetch(index0)
        x1 : Float64 = @delayLine.unsafe_fetch(index1)
        x2 : Float64 = @delayLine.unsafe_fetch(index2)
        x3 : Float64 = @delayLine.unsafe_fetch(index3)
        mu : Float64 = pos - index1

        ret : Float64 = case @interpolationMode
                        when .cubic? then Interpolate.cubic(x0, x1, x2, x3, mu)
                        when .hermite? then Interpolate.hermite(x0, x1, x2, x3, mu)
                        when .hermite_alt? then Interpolate.hermiteAlt(x0, x1, x2, x3, mu)
                        when .b_spline? then Interpolate.bspline(x0, x1, x2, x3, mu)
                        when .parabolic2_x? then Interpolate.parabolic2x(x0, x1, x2, x3, mu)
                        else Interpolate.linear(x1, x2, mu)
                        end
        #ret = ((1.0f64 - 0.7512746311209998f64) * ret) + (0.7512746311209998f64 * @lastFilter)
        ret = 0.2487253688790002 * ret + 0.7512746311209998 * @lastFilter

        @lastFilter = ret
        @delayLine.unsafe_put(@writePos, sample)
        @writePos += 1
        @writePos = 0 if @writePos == bufferLen

        ret
      end
    end

    @element1L : HeraElement
    @element1R : HeraElement
    @element2L : HeraElement
    @element2R : HeraElement
    @dcFilter1L : DCFilter = DCFilter.new
    @dcFilter1R : DCFilter = DCFilter.new
    @dcFilter2L : DCFilter = DCFilter.new
    @dcFilter2R : DCFilter = DCFilter.new

    # Creates a new instance of `HeraChorus`.
    def initialize(sampleRate : Int32, interpMode : Interpolate::Mode = Interpolate::Mode::Cubic)
      @element1L = HeraElement.new(sampleRate, 1.0, 0.5,  5.0)
      @element1R = HeraElement.new(sampleRate, 0.0, 0.5,  5.0)
      @element2L = HeraElement.new(sampleRate, 0.0, 0.73, 5.0)
      @element2R = HeraElement.new(sampleRate, 1.0, 0.73, 5.0)

      @element1L.interpolationMode = interpMode
      @element1R.interpolationMode = interpMode
      @element2L.interpolationMode = interpMode
      @element2R.interpolationMode = interpMode
    end

    # "Runs" the chorus on the given blocks of audio.
    def process(inputLeft : Array(Float64)|Slice(Float64), inputRight : Array(Float64)|Slice(Float64),
                outputLeft : Array(Float64)|Slice(Float64), outputRight : Array(Float64)|Slice(Float64)) : Nil
      unless inputLeft.size == outputLeft.size &&
             inputRight.size == outputRight.size &&
             inputLeft.size == inputRight.size
        raise BufferSizeMismatchError.new("The length of the buffers must match")
      end

      resL : Float64 = 0.0
      resR : Float64 = 0.0
      inL : Float64 = 0.0
      inR : Float64 = 0.0

      inputLeft.size.times do |i|
        inL = inputLeft[i]
        inR = inputRight[i]

        resL = @dcFilter1L.process(@element1L.process(inL)) + @element2L.process(inL)
        resR = @dcFilter1R.process(@element1R.process(inR)) + @element2R.process(inR)
        resL = @dcFilter2L.process(resL)
        resR = @dcFilter2R.process(resR)
        outputLeft[i] = resL
        outputRight[i] = resR
      end
    end

    # Mutes the chorus unit.
    @[AlwaysInline]
    def mute : Nil
      @element1L.mute
      @element1R.mute
      @element2L.mute
      @element2R.mute
      @dcFilter1L.reset
      @dcFilter1R.reset
      @dcFilter2L.reset
      @dcFilter2R.reset
    end
  end
end