// Copyright 2026 Everpaper. Licensed under the Apache License, Version 2.0 (see ../../../LICENSE). // // Sample-rate conversion to 16 kHz for the audio embedder: a polyphase Kaiser-windowed sinc low-pass computed in // Double, the same design and the same arithmetic as python/eg2_audio_frontend.py (pass band 0.913 and stop band // 1.0 of the lower Nyquist, 125 dB attenuation, unity DC gain, output length ceil(n * 16000 / rate) as librosa's // fix=True). The upstream pipeline uses libsoxr "HQ" instead; README "Audio: decoding and resampling" gives the // measured difference. Input already at 16 kHz is returned unchanged. import Accelerate import Foundation public enum AudioResampler { public static let targetRate = 16000 static let passband = 0.913 static let stopband = 1.0 static let attenuationDB = 125.0 /// Modified Bessel function of order 0 by its power series (identical to the Python version). static func besselI0(_ x: Double) -> Double { var s = 1.0, t = 1.0 var k = 1.0 let h = x / 2.0 while true { t *= (h / k) * (h / k) s += t if t < s * 1e-17 { return s } k += 1 } } /// Low-pass taps (odd length, unity DC gain) and the delay (len-1)/2 for in_rate -> in_rate*up/down. static func sincFilter(up: Int, down: Int, inRate: Int) -> (taps: [Double], delay: Int) { let nyqLow = Double(min(inRate, inRate * up / down)) / 2.0 let nyqHi = Double(inRate * up) / 2.0 let fc = 0.5 * (passband + stopband) * nyqLow / nyqHi let tw = (stopband - passband) * nyqLow / nyqHi let beta = 0.1102 * (attenuationDB - 8.7) var n = Int(ceil((attenuationDB - 7.95) / (2.285 * Double.pi * tw))) n |= 1 let c = (n - 1) / 2 let i0b = besselI0(beta) var h = [Double](repeating: 0, count: n) var sum = 0.0 for k in 0.. Polyphase { let (h, c) = sincFilter(up: up, down: down, inRate: inRate) var phases: [(j0: Int, rMin: Int, taps: [Double])] = [] for p in 0.. Int { b == 0 ? a : gcd(b, a % b) } /// Float samples at `rate` -> Float samples at 16 kHz. y[j] = up * sum_m x[m] h[c + j*down - m*up] in Double /// (each output is one vDSP double-precision dot product of a phase's taps with a reversed slice of the input). public static func resampleTo16k(_ x: [Float], rate: Int) -> [Float] { if rate == targetRate { return x } let nOut = Int(ceil(Double(x.count) * Double(targetRate) / Double(rate))) let g = gcd(targetRate, rate) let up = targetRate / g, down = rate / g let key = "\(up)/\(down)/\(rate)" lock.lock() var filt = cache[key] if filt == nil { filt = polyphase(up: up, down: down, inRate: rate) cache[key] = filt } lock.unlock() let pf = filt! let pad = (pf.phases.map { $0.taps.count }.max() ?? 0) + 1 // zero padding so no tap window leaves the buffer var xd = [Double](repeating: 0, count: x.count + 2 * pad) for i in 0..= 0 { let L = vDSP_Length(ph.taps.count) ph.taps.withUnsafeBufferPointer { hb in let rMax = ph.rMin + Int(L) - 1 var j = ph.j0 while j < nOut { let q = (j * down - p) / up // taps (reversed) k = 0 ... L-1 multiply x[q - rMax + k]; the padding keeps every index in range var acc = 0.0 vDSP_dotprD(xb.baseAddress! + (pad + q - rMax), 1, hb.baseAddress!, 1, &acc, L) y[j] = acc * scale j += up } } } } return y.map { Float($0) } } }