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