Artifact 5622af42305a1d26a01db8b456a7ab6e24c203241e06ae57541a805ad3e507e3:

  • File src/remiaudio/resampler/zoh.cr — part of check-in [37d34d6e0b] at 2024-07-10 21:27:43 on branch trunk — Fix compilation with -Dno_number_autocast (user: alexa size: 5928)

#### Based on libsamplerate
####
#### Copyright (c) 2002-2021, Erik de Castro Lopo <erikd@mega-nerd.com>
#### Copyright (c) 2024, Remilia Scarlet <remilia@posteo.jp>
#### All rights reserved.
####
#### This code is released under 2-clause BSD license. Please see the
#### file at : https://github.com/libsndfile/libsamplerate/blob/master/COPYING
require "./resampler"

module RemiAudio::Resampler
  # A Zero Order Hold converter (interpolated value is equal to the last value).
  # The quality is poor but the conversion speed is blindlingly fast.  Be aware
  # that this interpolator is not bandlimited, and the user is responsible for
  # adding anti-aliasing filtering.
  class ZohResampler < Resampler
    protected property? dirty : Bool = false
    protected property inCount : Int64 = 0i64
    protected property inUsed : Int64 = 0i64
    protected property outCount : Int64 = 0i64
    protected property outGen : Int64 = 0i64
    protected property lastValue : Array(Float64)

    # Creates a new `ZohResampler` that will handle the given number of
    # channels.
    def initialize(@channels : Int32)
      raise ArgumentError.new("Bad number of channels") unless @channels > 0
      @lastValue = Array(Float64).new(@channels, 0.0f32)
      reset
    end

    # :inherit:
    def reset : Nil
      super
      @dirty = false
      @lastValue.fill(0.0f32)
    end

    # Duplicates this resampler.
    def dup : ZohResampler
      ret = ZohResampler.new(@channels)
      ret.dirty = @dirty
      ret.inCount = @inCount
      ret.inUsed = @inUsed
      ret.outCount = @outCount
      ret.outGen = @outGen
      ret.lastValue = @lastValue.dup
      ret
    end

    protected def variProcess(data : Data) : Nil
      unless @dirty
        # If we have just been reset, set the last_value data.
        @channels.times do |ch|
          @lastValue[ch] = data.dataIn[ch].to_f64!
        end
        @dirty = true
      end

      @inCount = data.inputFrames * @channels
      @outCount = data.outputFrames * @channels
      @inUsed = 0
      @outGen = 0

      ratio : Float64 = @lastRatio
      inputIndex : Float64 = @lastPosition
      {% if flag?(:remiaudio_extrachecks) %}
        unless RemiAudio::Resampler.validRatio?(ratio)
          raise BadRatioError.new("Invalid ratio")
        end
      {% end %}

      # Calculate samples before first sample in input array.
      while inputIndex < 1.0 && @outGen < @outCount
        break if (@inUsed + @channels * (1.0 * inputIndex)) >= @inCount

        if @outCount > 0 && (@lastRatio - data.srcRatio).abs > SRC_MIN_RATIO_DIFF
          ratio = @lastRatio + @outGen * (data.srcRatio - @lastRatio) / @outCount
        end

        @channels.times do |ch|
          data.dataOut[@outGen] = @lastValue[ch].to_f32!
          @outGen += 1
        end

        # Figure out the next index.
        inputIndex += (1.0 / ratio)
      end

      rem : Float64 = fmodOne(inputIndex)
      @inUsed += @channels.to_i64! * (inputIndex - rem).round.to_i64!
      inputIndex = rem

      # Main processing loop
      while @outGen < @outCount && @inUsed + @channels * inputIndex <= @inCount
        if @outCount > 0 && (@lastRatio - data.srcRatio).abs > SRC_MIN_RATIO_DIFF
          ratio = @lastRatio + @outGen * (data.srcRatio - @lastRatio) / @outCount
        end

        @channels.times do |ch|
          data.dataOut[@outGen] = data.dataIn[@inUsed - @channels + ch]
          @outGen += 1
        end

        # Figure out the next index.
        inputIndex += 1.0 / ratio
        rem = fmodOne(inputIndex)

        @inUsed += @channels.to_i64! * (inputIndex - rem).round.to_i64!
        inputIndex = rem
      end

      if @inUsed > @inCount
        inputIndex += (@inUsed - @inCount) / @channels
        @inUsed = @inCount
      end

      @lastPosition = inputIndex

      if @inUsed > 0
        @channels.times do |ch|
          @lastValue[ch] = data.dataIn[@inUsed - @channels + ch].to_f64!
        end
      end

      # Save current ratio rather then target ratio.
      @lastRatio = ratio

      data.inputFramesUsed = @inUsed.tdiv(@channels)
      data.outputFramesGen = @outGen.tdiv(@channels)
    end
  end

  # A callback version of the `ZohResampler` class.
  class ZohResamplerCb < ZohResampler
    include CallbackResampler

    private def initialize(@channels : Int32)
      @callbackFunc = ->{ {Slice(Float32).empty, 0i64} }
      @lastValue = [] of Float64
      raise NotImplementedError.new("Use initialize(Int32, Float64, CallbackProc)")
    end

    # Creates a new `ZohResamplerCb` that will handle the given number of
    # channels.  The *ratio* parameter sets the initial ratio of the resampler
    # (`targetRate / sourceRate`), while *callbackFunc* is the method that will
    # be called whenever this resampler needs more input data.
    def initialize(@channels : Int32, ratio : Float64, @callbackFunc : CallbackProc)
      raise ArgumentError.new("Bad number of channels") unless @channels > 0
      @lastValue = Array(Float64).new(@channels, 0.0f32)
      reset
      @data.srcRatio = ratio
      self.ratio = ratio
    end

    # This is not supported by this class since `LinearResamplerCb` uses the
    # `CallbackResampler` mixin.  Calling this always raises a
    # `NotImplementedError`.
    def process(source : Array(Float32)|Slice(Float32), dest : Array(Float32)|Slice(Float32),
                ratio : Float64) : Tuple(Int64, Int64, Bool)
      raise NotImplementedError.new("Not supported by callback resamplers")
    end

    # :ditto:
    def process(source : Array(Float64)|Slice(Float64), dest : Array(Float64)|Slice(Float64),
                ratio : Float64) : Tuple(Int64, Int64, Bool)
      raise NotImplementedError.new("Not supported by callback resamplers")
    end

    protected def processData : Nil
      variProcess(@data)
    end

    # :inherit:
    def reset : Nil
      super
      @data.reset
    end
  end
end