Artifact 838ed7f1fe377a20bf653f49822c1323b4628429339de0a8fc15c847f8b526eb:

  • File src/remiaudio/dsp/softclipping.cr — part of check-in [97874c2fb3] at 2024-11-10 10:34:22 on branch trunk — Allow the soft clipping resample mode to be selected (user: alexa size: 11287)

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