///|
/// Streaming WSOLA. Channels share one alignment offset. The analysis clock
/// is anchored to output frames rather than previous correlation winners.
pub struct Processor {
channels : Int32
tempo : Double
hop : Int32
search : Int32
mut input : Array[Double]
mut base : Int64
mut received : Int64
mut emitted : Int64
mut tail : Array[Double]
mut started : Bool
mut finished : Bool
}
///|
/// A tempo of 2 halves duration without changing the intended pitch.
pub fn new(
sample_rate : Int32,
channels : Int32,
tempo : Double,
) -> Processor raise @types.AudioError {
@types.validate_format(sample_rate, channels)
@types.validate_ratio(tempo, 0.25, 4.0, "tempo")
{
channels,
tempo,
hop: sample_rate / 50,
search: sample_rate / 100,
input: [],
base: 0L,
received: 0L,
emitted: 0L,
tail: [],
started: false,
finished: false,
}
}
///|
/// Retained PCM including the overlap tail, measured in frames.
pub fn Processor::buffered_frames(self : Processor) -> Int32 {
(self.input.length() + self.tail.length()) / self.channels
}
///|
/// Startup lookahead in input frames; identity has no latency.
pub fn Processor::latency_frames(self : Processor) -> Int32 {
if self.tempo == 1.0 {
0
} else {
let clock = @math.ceil(self.hop.to_double() * self.tempo).to_int()
if clock > self.hop * 2 {
clock
} else {
self.hop * 2
}
}
}
///|
pub fn Processor::input_frames(self : Processor) -> Int64 {
self.received
}
///|
pub fn Processor::output_frames(self : Processor) -> Int64 {
self.emitted
}
///|
/// Start another stream with unchanged format and tempo.
pub fn Processor::reset(self : Processor) -> Unit {
self.input = []
self.tail = []
self.base = 0L
self.received = 0L
self.emitted = 0L
self.started = false
self.finished = false
}
///|
/// Out-of-stream samples are zero when draining the finite tail.
fn Processor::sample(
self : Processor,
frame : Int64,
channel : Int32,
) -> Double {
if frame < self.base || frame >= self.received {
0.0
} else {
self.input[(frame - self.base).to_int() * self.channels + channel]
}
}
///|
fn Processor::nominal(self : Processor) -> Int64 {
@math.floor(self.emitted.to_double() * self.tempo + 0.5).to_int64()
}
///|
/// Shared scaling prevents finite large PCM from overflowing correlation.
fn Processor::score(
self : Processor,
position : Int64,
stride : Int32,
scale : Double,
reference_energy : Double,
) -> Double {
if scale == 0.0 || reference_energy == 0.0 {
return 0.0
}
let mut product = 0.0
let mut energy = 0.0
let mut frame = 0
while frame < self.hop {
for channel = 0; channel < self.channels; channel = channel + 1 {
let reference = self.tail[frame * self.channels + channel] / scale
let candidate = self.sample(position + frame.to_int64(), channel) / scale
product = product + reference * candidate
energy = energy + candidate * candidate
}
frame = frame + stride
}
if energy == 0.0 {
0.0
} else {
product / (reference_energy.sqrt() * energy.sqrt())
}
}
///|
fn Processor::reference_energy(
self : Processor,
stride : Int32,
scale : Double,
) -> Double {
if scale == 0.0 {
return 0.0
}
let mut result = 0.0
let mut frame = 0
while frame < self.hop {
for channel = 0; channel < self.channels; channel = channel + 1 {
let value = self.tail[frame * self.channels + channel] / scale
result = result + value * value
}
frame = frame + stride
}
result
}
///|
fn closer(candidate : Int64, current : Int64, nominal : Int64) -> Bool {
let a = if candidate >= nominal {
candidate - nominal
} else {
nominal - candidate
}
let b = if current >= nominal { current - nominal } else { nominal - current }
a < b || (a == b && candidate < current)
}
///|
/// Sparse correlations locate a region; every frame in it is then checked
/// using the full overlap. Equal scores prefer the nominal analysis clock.
fn Processor::choose(self : Processor, nominal : Int64) -> Int64 {
let low = if nominal > self.search.to_int64() {
nominal - self.search.to_int64()
} else {
0L
}
let high = nominal + self.search.to_int64()
let mut scale = 0.0
let mut scale_frame = low
while scale_frame < high + self.hop.to_int64() {
for channel = 0; channel < self.channels; channel = channel + 1 {
let magnitude = self.sample(scale_frame, channel).abs()
if magnitude > scale {
scale = magnitude
}
}
scale_frame = scale_frame + 1L
}
for value in self.tail {
let magnitude = value.abs()
if magnitude > scale {
scale = magnitude
}
}
if scale == 0.0 {
return nominal
}
let stride = (self.hop + 127) / 128
let step = (self.search * 2 + 95) / 96
let ref_coarse = self.reference_energy(stride, scale)
let mut best = nominal
let mut best_score = self.score(nominal, stride, scale, ref_coarse)
let mut position = low
while position <= high {
let score = self.score(position, stride, scale, ref_coarse)
if score > best_score + 1.0e-12 ||
((score - best_score).abs() <= 1.0e-12 && closer(position, best, nominal)) {
best = position
best_score = score
}
position = position + step.to_int64()
}
let refine_low = if best - step.to_int64() > low {
best - step.to_int64()
} else {
low
}
let refine_high = if best + step.to_int64() < high {
best + step.to_int64()
} else {
high
}
let ref_full = self.reference_energy(1, scale)
best_score = self.score(best, 1, scale, ref_full)
position = refine_low
while position <= refine_high {
let score = self.score(position, 1, scale, ref_full)
if score > best_score + 1.0e-12 ||
((score - best_score).abs() <= 1.0e-12 && closer(position, best, nominal)) {
best = position
best_score = score
}
position = position + 1L
}
best
}
///|
/// Release PCM that cannot be selected by any later analysis window.
fn Processor::compact(self : Processor) -> Unit {
if !self.started {
return
}
let nominal = self.nominal()
let keep = if nominal > self.search.to_int64() {
nominal - self.search.to_int64()
} else {
0L
}
let limit = if keep < self.received { keep } else { self.received }
if limit > self.base {
let drop = (limit - self.base).to_int() * self.channels
self.input = self.input[drop:].to_owned()
self.base = limit
}
}
///|
/// Convex interpolation without overflow when both finite endpoints have the
/// same sign and are close to the maximum representable floating point value.
fn blend(a : Double, b : Double, mix : Double) -> Double {
if (a >= 0.0 && b >= 0.0) || (a <= 0.0 && b <= 0.0) {
if a >= b {
a - (a - b) * mix
} else {
b - (b - a) * (1.0 - mix)
}
} else {
a * (1.0 - mix) + b * mix
}
}
///|
fn Processor::emit(
self : Processor,
frames : Int32,
output : Array[Double],
) -> Unit {
let position = if self.started { self.choose(self.nominal()) } else { 0L }
for frame = 0; frame < frames; frame = frame + 1 {
let mix = frame.to_double() / self.hop.to_double()
for channel = 0; channel < self.channels; channel = channel + 1 {
let current = self.sample(position + frame.to_int64(), channel)
output.push(
if self.started {
blend(self.tail[frame * self.channels + channel], current, mix)
} else {
current
},
)
}
}
self.tail = Array::makei(self.hop * self.channels, fn(index) {
self.sample(
position + (self.hop + index / self.channels).to_int64(),
index % self.channels,
)
})
self.started = true
self.emitted = self.emitted + frames.to_int64()
self.compact()
}
///|
fn Processor::ready(self : Processor) -> Bool {
let needed = if self.started {
self.nominal() + (self.search + self.hop * 2).to_int64()
} else {
(self.hop * 2).to_int64()
}
needed <= self.received &&
(self.emitted + self.hop.to_int64()).to_double() * self.tempo <=
self.received.to_double()
}
///|
/// Consume complete frames, returning available PCM. Validation errors leave
/// state unchanged. A large input is internally consumed in short blocks.
pub fn Processor::push(
self : Processor,
samples : Array[Double],
) -> Array[Double] raise @types.AudioError {
if self.finished {
raise @types.AudioError("push after finish; call reset first")
}
@types.validate_samples(samples, self.channels)
let frames = samples.length() / self.channels
if self.received > 1_000_000_000_000L - frames.to_int64() {
raise @types.AudioError("stream exceeds exact analysis clock range")
}
if self.tempo == 1.0 {
self.received = self.received + frames.to_int64()
self.emitted = self.received
return samples.copy()
}
let output : Array[Double] = []
let mut cursor = 0
let block = self.hop * self.channels
while cursor < samples.length() {
let end = if samples.length() - cursor > block {
cursor + block
} else {
samples.length()
}
for index = cursor; index < end; index = index + 1 {
self.input.push(samples[index])
}
self.received = self.received + ((end - cursor) / self.channels).to_int64()
while self.ready() {
self.emit(self.hop, output)
}
self.compact()
cursor = end
}
output
}
///|
/// Drain with zero extension to exactly round(input/tempo) frames.
/// Repeated finish calls return empty output; reset starts a new stream.
pub fn Processor::finish(self : Processor) -> Array[Double] {
if self.finished {
return []
}
self.finished = true
let target = @math.floor(self.received.to_double() / self.tempo + 0.5).to_int64()
let output : Array[Double] = []
while self.emitted < target {
let remaining = target - self.emitted
let frames = if remaining > self.hop.to_int64() {
self.hop
} else {
remaining.to_int()
}
self.emit(frames, output)
}
self.input = []
self.tail = []
self.base = self.received
output
}