#### RemiAudio
#### Copyright (C) 2022-2024 Remilia Scarlet <remilia@posteo.jp>
####
#### 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/>.
#### RemiAudio
#### Copyright (C) 2022-2024 Remilia Scarlet <remilia@posteo.jp>
####
#### 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 "../resampler"
module RemiAudio::DSP
# The `SoftClipper` class implements a type of limiter with a type of
# distortion that tends to be more pleasant to the ears than straight
# clipping. This will round off the peaks of the audio signal that pass above
# the maximum amplitude rather than cut them off flat.
#
# A simplified diagram can be found
# [here](https://chiselapp.com/user/MistressRemilia/repository/remiaudio/uv/soft-clipping.png).
class SoftClipper
@upsampler : RemiAudio::Resampler::Resampler?
@downsampler : RemiAudio::Resampler::Resampler?
@resampBuf : Array(Float64) = [] of Float64
@resampBuf32 : Array(Float32) = [] of Float32
getter oversampling : UInt16
getter mode : RemiAudio::Resampler::Type
# Creates a new `SoftClipper` instance that will work with the givven number
# of channels, and with the given amount of oversampling. The oversampling
# amount cannot be zero.
#
# When oversampling is `1`, then no oversampling is performed. This is the
# default to match the behavior of older versions of RemiAudio.
def initialize(*, @oversampling : UInt16 = 1u16, @channels : Int32 = 2,
@mode : RemiAudio::Resampler::Type = RemiAudio::Resampler::Type::Linear)
if @oversampling == 0
raise ArgumentError.new("Oversampling cannot be zero")
end
end
# Sets a new oversampling amount. This cannot be zero.
def oversampling=(@oversampling : UInt16) : UInt16
if @oversampling == 0
raise ArgumentError.new("Oversampling cannot be zero")
end
@oversampling
end
# Sets the resampling mode. This is not thread-safe.
def mode=(@mode : RemiAudio::Resampler::Type) : Nil
@upsampler = nil
@downsampler = nil
end
############################################################################
@[AlwaysInline]
protected def ensureResampBufSize(size : Int) : Nil
if @resampBuf.size != size * @oversampling
@resampBuf = Array(Float64).new(size * @oversampling, 0.0)
end
end
@[AlwaysInline]
protected def ensureResampBufSize32(size : Int) : Nil
if @resampBuf32.size != size * @oversampling
@resampBuf32 = Array(Float32).new(size * @oversampling, 0.0f32)
end
end
@[AlwaysInline]
private def getResampler : RemiAudio::Resampler::Resampler
case @mode
in .linear? then RemiAudio::Resampler::LinearResampler.new(@channels)
in .zero_order_hold? then RemiAudio::Resampler::LinearResampler.new(@channels)
in .sinc_fastest?
unless @channels == 2
raise RemiAudioError.new("Cannot use Sinc resampling unless channels is 2")
end
RemiAudio::Resampler::SincResamplerStereo.new(RemiAudio::Resampler::SincResampler::Quality::Fast)
in .sinc_medium?
unless @channels == 2
raise RemiAudioError.new("Cannot use Sinc resampling unless channels is 2")
end
RemiAudio::Resampler::SincResamplerStereo.new(RemiAudio::Resampler::SincResampler::Quality::Medium)
in .sinc_best?
unless @channels == 2
raise RemiAudioError.new("Cannot use Sinc resampling unless channels is 2")
end
RemiAudio::Resampler::SincResamplerStereo.new(RemiAudio::Resampler::SincResampler::Quality::Best)
end
end
@[AlwaysInline]
protected def getUpsampler : RemiAudio::Resampler::Resampler
resamp = @upsampler
if resamp.nil?
resamp = @upsampler = getResampler
end
resamp
end
@[AlwaysInline]
protected def getDownsampler : RemiAudio::Resampler::Resampler
resamp = @downsampler
if resamp.nil?
resamp = @downsampler = getResampler
end
resamp
end
# Performs upsampling on *block*, storing the results in `@resampBuf`.
@[AlwaysInline]
protected def upsample(block : Array(Float64)|Slice(Float64)) : Nil
return if @oversampling <= 1
# Ensure our resampling buf is the correct size, then resample.
ensureResampBufSize(block.size)
getUpsampler.process(block, @resampBuf, @oversampling.to_f64!)
end
# Performs upsampling on *block*, storing the results in `@resampBuf`.
@[AlwaysInline]
protected def upsample(block : Array(Float32)|Slice(Float32)) : Nil
return if @oversampling <= 1
# Ensure our resampling buf is the correct size, then resample.
ensureResampBufSize32(block.size)
getUpsampler.process(block, @resampBuf32, @oversampling.to_f64!)
end
@[AlwaysInline]
protected def downsample(block : Array(Float64)|Slice(Float64)) : Nil
return if @oversampling <= 1
# Consistency checks.
RemiLib.assert(@resampBuf.size == block.size * @oversampling)
# Downsample
getDownsampler.process(@resampBuf, block, 1 / @oversampling)
end
@[AlwaysInline]
protected def downsample(block : Array(Float32)|Slice(Float32)) : Nil
return if @oversampling <= 1
# Consistency checks.
RemiLib.assert(@resampBuf32.size == block.size * @oversampling)
# Downsample
getDownsampler.process(@resampBuf32, block, 1 / @oversampling)
end
############################################################################
# Applies soft clipping to a block of audio.
def process(maxAmplitude : Float32|Float64, block : Array(Float64)|Slice(Float64)) : Nil
if @oversampling > 1
upsample(block)
SoftClipper.process(maxAmplitude, @resampBuf)
downsample(block)
else
SoftClipper.process(maxAmplitude, block)
end
end
# Applies soft clipping to a block of audio.
def process(maxAmplitude : Float32|Float64, block : Array(Float32)|Slice(Float32)) : Nil
if @oversampling > 1
#storeAsFloat64(block)
upsample(block)
SoftClipper.process(maxAmplitude, @resampBuf32)
downsample(block)
#retrieveFloat32(block)
else
SoftClipper.process(maxAmplitude, block)
end
end
############################################################################
# Applies soft clipping to a single sample. No oversampling is performed.
@[AlwaysInline]
def self.process(maxAmplitude : Float32|Float64, sample : Float64) : Float64
maxAmp : Float64 = maxAmplitude.to_f64!
sign : Int8 = sample >= 0 ? 1i8 : -1i8
absSample : Float64 = sample.abs
if absSample > 1.0
sign * ((maxAmp + 1) * 0.5)
elsif absSample > maxAmp
sign * (maxAmp + (absSample - maxAmp) /
((((absSample - maxAmp) / (maxAmp - 1)) ** 2) + 1))
else
sample
end
end
# Applies soft clipping to a single sample. No oversampling is performed.
@[AlwaysInline]
def self.process(maxAmplitude : Float32|Float64, sample : Float32) : Float32
maxAmp : Float32 = maxAmplitude.to_f32!
sign : Int8 = sample >= 0 ? 1i8 : -1i8
absSample : Float32 = sample.abs
if absSample > 1.0f32
sign * ((maxAmp + 1) * 0.5f32)
elsif absSample > maxAmp
sign * (maxAmp + (absSample - maxAmp) /
((((absSample - maxAmp) / (maxAmp - 1)) ** 2) + 1))
else
sample
end
end
# # Applies soft clipping to a single sample. No oversampling is performed.
# @[AlwaysInline]
# def self.process(maxAmplitude : Float32|Float64, sample : Float32) : Float32
# process(maxAmplitude, sample.to_f64!).to_f32!
# end
# Applies soft clipping to a buffer of audio. No oversampling is performed.
@[AlwaysInline]
def self.process(maxAmplitude : Float32|Float64, samples : Array(Float64)|Slice(Float64)) : Nil
samples.size.times do |i|
samples.unsafe_put(i, process(maxAmplitude, samples.unsafe_fetch(i)))
end
end
# Applies soft clipping to a buffer of audio. No oversampling is performed.
@[AlwaysInline]
def self.process(maxAmplitude : Float32|Float64, samples : Array(Float32)|Slice(Float32)) : Nil
samples.size.times do |i|
samples.unsafe_put(i, process(maxAmplitude, samples.unsafe_fetch(i).to_f64!).to_f32!)
end
end
end
end
# The Soft Clipping module implements a type of limiter with a type of
# distortion that tends to be more pleasant to the ears than straight clipping.
# This will round off the peaks of the audio signal that pass above the maximum
# amplitude rather than cut them off flat.
#
# A simplified diagram can be found
# [here](https://chiselapp.com/user/MistressRemilia/repository/remiaudio/uv/soft-clipping.png).
module RemiAudio::DSP::SoftClipping
# Applies soft clipping to the two blocks of audio.
@[Deprecated("Use the new SoftClipper class")]
@[AlwaysInline]
def self.process(maxAmplitude : Float32|Float64, blockLeft : Array(Float64)|Slice(Float64),
blockRight : Array(Float64)|Slice(Float64)) : Nil
{% unless flag?(:remiaudio_wd40) %}
unless blockLeft.size == blockRight.size
raise RemiAudioError.new("Block sizes differ")
end
{% end %}
SoftClipper.process(maxAmplitude, blockLeft)
SoftClipper.process(maxAmplitude, blockRight)
end
# Applies soft clipping to a single sample.
@[Deprecated("Use the new SoftClipper class")]
@[AlwaysInline]
def self.process(maxAmplitude : Float32|Float64, sample : Float64) : Float64
SoftClipper.process(maxAmplitude, sample)
end
# Applies soft clipping to a single sample.
@[Deprecated("Use the new SoftClipper class")]
@[AlwaysInline]
def self.process(maxAmplitude : Float32|Float64, sample : Float32) : Float32
SoftClipper.process(maxAmplitude, sample)
end
end