File size: 5,592 Bytes
305fc2a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// 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..<n {
            let m = Double(k - c)
            let sinc = (k == c) ? fc : sin(Double.pi * fc * m) / (Double.pi * m)
            let r = 2.0 * Double(k) / Double(n - 1) - 1.0
            let w = besselI0(beta * sqrt(max(0.0, 1.0 - r * r))) / i0b
            h[k] = sinc * w
            sum += h[k]
        }
        for k in 0..<n { h[k] /= sum }
        return (h, c)
    }

    /// Per-phase tap vectors: phase p (= (j*down) % up) uses h[c + p + r*up] for r = rMin(p) ... rMax(p), stored in
    /// increasing r and applied to x[q - r] (decreasing sample index), q = (j*down - p) / up.
    struct Polyphase {
        let phases: [(j0: Int, rMin: Int, taps: [Double])]   // j0 = first output index of the phase, -1 = unused
    }

    static func polyphase(up: Int, down: Int, inRate: Int) -> Polyphase {
        let (h, c) = sincFilter(up: up, down: down, inRate: inRate)
        var phases: [(j0: Int, rMin: Int, taps: [Double])] = []
        for p in 0..<up {
            var j0 = -1
            for j in 0..<up where (j * down) % up == p { j0 = j; break }
            let rMin = -((c + p) / up)
            let rMax = (h.count - 1 - c - p) / up
            // stored REVERSED (r = rMax first) so that the dot product walks the input forward from x[q - rMax]
            phases.append((j0, rMin, j0 < 0 ? [] : (rMin...rMax).reversed().map { h[c + p + $0 * up] }))
        }
        return Polyphase(phases: phases)
    }

    // guarded by `lock` (filters are computed once per input rate and shared)
    nonisolated(unsafe) private static var cache: [String: Polyphase] = [:]
    private static let lock = NSLock()

    static func gcd(_ a: Int, _ b: Int) -> 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..<x.count { xd[pad + i] = Double(x[i]) }
        var y = [Double](repeating: 0, count: nOut)
        let scale = Double(up)
        xd.withUnsafeBufferPointer { xb in
            for (p, ph) in pf.phases.enumerated() where ph.j0 >= 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) }
    }
}