#### 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 linear converter. The quality is poor, but the conversion speed is
# blindingly fast. This interpolator is also not bandlimited, and the user is
# responsible for adding anti-aliasing filtering.
class LinearResampler < 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 `LinearResampler` 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 : LinearResampler
ret = LinearResampler.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] +
inputIndex *
(data.dataIn[ch].to_f64! - @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
{% if flag?(:remiaudio_debug) %}
if @inUsed < @channels && inputIndex < 1.0
raise "Whoops!!!! in_used: #{@inUsed} channels: #{@channels} inputIndex: #{inputIndex}"
end
{% end %}
@channels.times do |ch|
data.dataOut[@outGen] = (data.dataIn[@inUsed - @channels + ch] +
inputIndex *
(data.dataIn[@inUsed + ch].to_f64! -
data.dataIn[@inUsed - @channels + ch])).to_f32!
@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 `LinearResampler` class.
class LinearResamplerCb < LinearResampler
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 `LinearResamplerCb` 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