///|
/// Compute the resampling ratio (source_rate / output_rate).
/// Ratio > 1.0 means downsampling, < 1.0 means upsampling.
fn resample_ratio(source_rate : Int, output_rate : Int) -> Float {
Float::from_int(source_rate) / Float::from_int(output_rate)
}
///|
/// Read a resampled frame from a source at a fractional position.
fn resample_frame(
quality : ResampleQuality,
source : AudioSource,
position : Float,
channel : Int,
frame_count : Int,
) -> Float {
match quality {
Nearest => resample_nearest(source, position, channel, frame_count)
Linear => resample_linear(source, position, channel, frame_count)
Cubic => resample_cubic(source, position, channel, frame_count)
}
}
///|
fn clamp_frame(frame : Int, frame_count : Int) -> Int {
if frame < 0 {
0
} else if frame >= frame_count {
frame_count - 1
} else {
frame
}
}
///|
fn resample_nearest(
source : AudioSource,
position : Float,
channel : Int,
frame_count : Int,
) -> Float {
let frame = clamp_frame((position + 0.5).to_int(), frame_count)
read_source_sample(source, frame, channel)
}
///|
fn resample_linear(
source : AudioSource,
position : Float,
channel : Int,
frame_count : Int,
) -> Float {
let floor_pos = position.to_int()
let frac = position - Float::from_int(floor_pos)
let i0 = clamp_frame(floor_pos, frame_count)
let i1 = clamp_frame(floor_pos + 1, frame_count)
let s0 = read_source_sample(source, i0, channel)
let s1 = read_source_sample(source, i1, channel)
s0 + (s1 - s0) * frac
}
///|
fn resample_cubic(
source : AudioSource,
position : Float,
channel : Int,
frame_count : Int,
) -> Float {
let floor_pos = position.to_int()
let frac = position - Float::from_int(floor_pos)
let i0 = clamp_frame(floor_pos - 1, frame_count)
let i1 = clamp_frame(floor_pos, frame_count)
let i2 = clamp_frame(floor_pos + 1, frame_count)
let i3 = clamp_frame(floor_pos + 2, frame_count)
let s0 = read_source_sample(source, i0, channel)
let s1 = read_source_sample(source, i1, channel)
let s2 = read_source_sample(source, i2, channel)
let s3 = read_source_sample(source, i3, channel)
// Hermite interpolation
let c0 = s1
let c1 = (s2 - s0) * 0.5
let c2 = s0 - s1 * 2.5 + s2 * 2.0 - s3 * 0.5
let c3 = (s3 - s0) * 0.5 + (s1 - s2) * 1.5
((c3 * frac + c2) * frac + c1) * frac + c0
}
///|
test "resample_ratio identity" {
let ratio = resample_ratio(44100, 44100)
let tol : Float = 0.0001
assert_true(Float::is_close(ratio, 1.0, absolute_tolerance=tol))
}
///|
test "resample_ratio upsample" {
let ratio = resample_ratio(22050, 44100)
let tol : Float = 0.0001
assert_true(Float::is_close(ratio, 0.5, absolute_tolerance=tol))
}
///|
test "resample_ratio downsample" {
let ratio = resample_ratio(88200, 44100)
let tol : Float = 0.0001
assert_true(Float::is_close(ratio, 2.0, absolute_tolerance=tol))
}
///|
test "resample identity (44100->44100) matches direct read" {
let buf = new_audio_buffer(1, 44100, 4)
set_sample(buf, 0, 0, 0.1)
set_sample(buf, 1, 0, 0.4)
set_sample(buf, 2, 0, 0.9)
set_sample(buf, 3, 0, -0.2)
let src = AudioSource::Buffer(buf)
let tol : Float = 0.0001
// At integer positions, Linear should match direct sample
for i in 0..<4 {
let pos = Float::from_int(i)
let resampled = resample_frame(Linear, src, pos, 0, 4)
let direct = read_source_sample(src, i, 0)
assert_true(Float::is_close(resampled, direct, absolute_tolerance=tol))
}
}
///|
test "resample linear upsample (22050->44100)" {
// 2 frames [0.0, 1.0] upsampled 2x should give [0.0, 0.5, 1.0, 1.0(clamped)]
let buf = new_audio_buffer(1, 22050, 2)
set_sample(buf, 0, 0, 0.0)
set_sample(buf, 1, 0, 1.0)
let src = AudioSource::Buffer(buf)
let tol : Float = 0.0001
// position 0.0 -> sample 0.0
let s0 = resample_frame(Linear, src, 0.0, 0, 2)
assert_true(Float::is_close(s0, 0.0, absolute_tolerance=tol))
// position 0.5 -> interpolated 0.5
let s1 = resample_frame(Linear, src, 0.5, 0, 2)
assert_true(Float::is_close(s1, 0.5, absolute_tolerance=tol))
// position 1.0 -> sample 1.0
let s2 = resample_frame(Linear, src, 1.0, 0, 2)
assert_true(Float::is_close(s2, 1.0, absolute_tolerance=tol))
}
///|
test "resample boundary clamp no panic" {
let buf = new_audio_buffer(1, 44100, 2)
set_sample(buf, 0, 0, 0.5)
set_sample(buf, 1, 0, 1.0)
let src = AudioSource::Buffer(buf)
// Should not panic even with out-of-range positions
let _ = resample_frame(Nearest, src, -1.0, 0, 2)
let _ = resample_frame(Linear, src, 10.0, 0, 2)
let _ = resample_frame(Cubic, src, -2.0, 0, 2)
let _ = resample_frame(Cubic, src, 100.0, 0, 2)
}
///|
test "resample nearest" {
let buf = new_audio_buffer(1, 44100, 3)
set_sample(buf, 0, 0, 0.0)
set_sample(buf, 1, 0, 1.0)
set_sample(buf, 2, 0, 0.5)
let src = AudioSource::Buffer(buf)
let tol : Float = 0.0001
// position 0.3 -> nearest is frame 0
let s0 = resample_frame(Nearest, src, 0.3, 0, 3)
assert_true(Float::is_close(s0, 0.0, absolute_tolerance=tol))
// position 0.6 -> nearest is frame 1
let s1 = resample_frame(Nearest, src, 0.6, 0, 3)
assert_true(Float::is_close(s1, 1.0, absolute_tolerance=tol))
}