///|
/// Stateless equal-power pan processor for mono-to-stereo routing.
pub struct Pan {}
///|
/// Create a pan processor.
#alias(new)
pub fn Pan::Pan() -> Pan {
Pan::{ }
}
///|
/// Pan a mono input buffer into explicit left and right output buffers.
///
/// Finite positions are clamped to `[-1.0, 1.0]`, where `-1.0` is hard left,
/// `0.0` is center, and `1.0` is hard right. Invalid positions or contexts
/// write silence to both outputs.
pub fn Pan::process(
self : Pan,
context~ : DspContext,
input~ : AudioBuffer,
left_output~ : AudioBuffer,
right_output~ : AudioBuffer,
position~ : Double,
) -> Unit {
ignore(self)
let sample_rate = context.sample_rate()
let sample_count = pan_sample_count(context, input, left_output, right_output)
if !is_finite_positive(sample_rate) ||
!is_finite(position) ||
sample_count <= 0 {
left_output.fill(0.0)
right_output.fill(0.0)
return
}
let left_gain = pan_left_gain(position)
let right_gain = pan_right_gain(position)
for index = 0; index < sample_count; index = index + 1 {
let sample = input.get(index)
left_output.set(index, sample * left_gain)
right_output.set(index, sample * right_gain)
}
for index = sample_count; index < left_output.length(); index = index + 1 {
left_output.set(index, 0.0)
}
for index = sample_count; index < right_output.length(); index = index + 1 {
right_output.set(index, 0.0)
}
}
///|
/// Equal-power left-channel gain for a pan position in [-1.0, 1.0].
/// -1.0 = hard left (gain 1.0), 0.0 = center (~0.707), 1.0 = hard right (gain 0.0).
/// Non-finite positions return 0.0.
pub fn pan_left_gain(position : Double) -> Double {
if !is_finite(position) {
0.0
} else {
let clamped = position.clamp(min=-1.0, max=1.0)
if clamped <= -1.0 {
1.0
} else if clamped >= 1.0 {
0.0
} else {
let angle = (clamped + 1.0) * @math.PI * 0.25
@math.cos(angle)
}
}
}
///|
/// Equal-power right-channel gain for a pan position in [-1.0, 1.0].
/// -1.0 = hard left (gain 0.0), 0.0 = center (~0.707), 1.0 = hard right (gain 1.0).
/// Non-finite positions return 0.0.
pub fn pan_right_gain(position : Double) -> Double {
if !is_finite(position) {
0.0
} else {
let clamped = position.clamp(min=-1.0, max=1.0)
if clamped <= -1.0 {
0.0
} else if clamped >= 1.0 {
1.0
} else {
let angle = (clamped + 1.0) * @math.PI * 0.25
@math.sin(angle)
}
}
}
///|
fn pan_sample_count(
context : DspContext,
input : AudioBuffer,
left_output : AudioBuffer,
right_output : AudioBuffer,
) -> Int {
let block_size = context.block_size()
let input_len = input.length()
let left_len = left_output.length()
let right_len = right_output.length()
let bounded = if input_len < left_len {
if input_len < right_len {
input_len
} else {
right_len
}
} else if left_len < right_len {
left_len
} else {
right_len
}
if bounded < block_size {
bounded
} else {
block_size
}
}