File size: 9,827 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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
// Copyright 2026 Everpaper. Licensed under the Apache License, Version 2.0 (see ../../../LICENSE).
//
// Audio front end: samples -> 16 kHz mono -> log-mel frames -> the inputs of `audio_<Tb>`. The feature extraction
// re-implements the defaults of Gemma4AudioFeatureExtractor (feature_extraction_gemma4.py in Hugging Face
// transformers 5.19, Copyright 2026 Google LLC, Apache-2.0) and the HTK mel filter-bank formula of transformers'
// audio_utils.py (Copyright 2023 The HuggingFace Inc. team and the librosa & torchaudio authors, Apache-2.0; the
// formula as in librosa / torchaudio) exactly as python/eg2_audio_frontend.py does in numpy:
//   first 30 s (480 000 samples), right pad to a multiple of 128 samples, 160 zeros in front ("semicausal"),
//   frames of 320 samples every 160 (the window is periodic Hann), |rfft(512)| in single precision, 128 triangular
//   HTK mel filters 0-8000 Hz (no normalization) applied in double precision, ln(mel + 0.001) cast to float32;
//   a frame is valid only if its 321-sample analysis window lies in real audio (at most 2999 frames).
// The FFT is Accelerate's; its single-precision rounding differs from numpy's pocketfft by ~1e-6 (README).

import Accelerate
import Foundation

/// The inputs of one `audio_<bucket>` call (see README "How an audio clip becomes a vector").
public struct AudioInputs: Sendable {
    public static let nMels = 128
    public let bucket: Int            // frames of the function (192, 384, 1024, 1536 or 3000)
    public let frameCount: Int        // valid log-mel frames (10 ms each)
    public let softTokenCount: Int    // ceil(ceil(frames / 2) / 2)
    public let features: [Float16]    // [bucket * 128], rows >= frameCount are zero
    public let frameMask: [Float16]   // [bucket]
    public let halfMask: [Float16]    // [bucket / 2]
    public let tokenMask: [Float16]   // [bucket / 4]
}

public enum AudioFrontend {
    public static let sampleRate = 16000
    public static let frameLength = 320
    public static let hopLength = 160
    public static let fftLength = 512
    public static let nMels = 128
    public static let melFloor = 1e-3
    public static let maxSamples = 480_000          // 30 s, the upstream default truncation
    public static let padMultiple = 128
    public static let maxFrames = 2999
    public static let frameBuckets = [192, 384, 1024, 1536, 3000]
    public static let maxSoftTokens = 750

    // MARK: - Mono mix

    /// Mean of the channels in Float, summed in channel order (the numpy reference does the same).
    public static func toMono(_ audio: DecodedAudio) -> [Float] {
        guard audio.channelCount > 1 else { return audio.channels.first ?? [] }
        var acc = audio.channels[0]
        for c in 1..<audio.channelCount {
            let ch = audio.channels[c]
            for i in 0..<acc.count { acc[i] += ch[i] }
        }
        let inv = Float(audio.channelCount)
        for i in 0..<acc.count { acc[i] /= inv }
        return acc
    }

    /// decode -> mono -> 16 kHz, the librosa.load(sr=16000, mono=True) order.
    public static func loadMono16k(url: URL) throws -> [Float] {
        let d = try AudioDecoder.decode(url: url)
        return AudioResampler.resampleTo16k(toMono(d), rate: d.sampleRate)
    }

    // MARK: - Frame counts

    /// Frames whose whole 321-sample analysis window (with the 160-sample left pad) lies in real audio.
    public static func validFrameCount(samples n: Int) -> Int {
        frameCount(samples: min(n, maxSamples))
    }

    /// (n + 160 - 321) floor-div 160 + 1, zero when the first window does not fit (n < 161).
    static func frameCount(samples n: Int) -> Int {
        let d = n + frameLength / 2 - (frameLength + 1)
        return d < 0 ? 0 : d / hopLength + 1
    }

    public static func softTokenCount(frames: Int) -> Int { ((frames + 1) / 2 + 1) / 2 }

    public static func bucket(forFrames n: Int) throws -> Int {
        guard let b = frameBuckets.first(where: { $0 >= n }) else {
            throw EmbedderError(description: "\(n) frames exceed the largest bucket \(frameBuckets.last ?? 0)")
        }
        return b
    }

    // MARK: - Mel filters and window (computed once, double precision like the reference)

    static let melFilters: [Double] = {   // [257 * 128], row-major (bin, mel)
        func hzToMel(_ f: Double) -> Double { 2595.0 * log10(1.0 + f / 700.0) }
        func melToHz(_ m: Double) -> Double { 700.0 * (pow(10.0, m / 2595.0) - 1.0) }
        let nBins = fftLength / 2 + 1
        let mLo = hzToMel(0.0), mHi = hzToMel(8000.0)
        var filterFreqs = [Double](repeating: 0, count: nMels + 2)
        for i in 0..<(nMels + 2) {   // numpy.linspace: start + i * step, last = stop
            let step = (mHi - mLo) / Double(nMels + 1)
            filterFreqs[i] = melToHz(i == nMels + 1 ? mHi : mLo + Double(i) * step)
        }
        var fftFreqs = [Double](repeating: 0, count: nBins)
        for i in 0..<nBins {
            let step = Double(sampleRate / 2) / Double(nBins - 1)
            fftFreqs[i] = i == nBins - 1 ? Double(sampleRate / 2) : Double(i) * step
        }
        var diff = [Double](repeating: 0, count: nMels + 1)
        for i in 0..<(nMels + 1) { diff[i] = filterFreqs[i + 1] - filterFreqs[i] }
        var w = [Double](repeating: 0, count: nBins * nMels)
        for b in 0..<nBins {
            for m in 0..<nMels {
                let down = -(filterFreqs[m] - fftFreqs[b]) / diff[m]
                let up = (filterFreqs[m + 2] - fftFreqs[b]) / diff[m + 1]
                w[b * nMels + m] = max(0.0, min(down, up))
            }
        }
        return w
    }()

    static let hannWindow: [Float] = (0..<frameLength).map { n in
        Float(0.5 - 0.5 * cos(2.0 * Double.pi * Double(n) / Double(frameLength)))
    }

    // MARK: - Log-mel

    /// Log-mel features of a 16 kHz mono signal: [validFrames * 128] Float, row-major, at most 2999 frames.
    public static func logMel(_ wav16k: [Float], maxSamples limit: Int? = maxSamples) -> (features: [Float], frames: Int) {
        let x = limit.map { Array(wav16k.prefix($0)) } ?? wav16k
        let n = x.count
        let m = frameCount(samples: n)
        if m <= 0 { return ([], 0) }
        let nPad = (n + padMultiple - 1) / padMultiple * padMultiple
        var xp = [Float](repeating: 0, count: frameLength / 2 + nPad)
        for i in 0..<n { xp[frameLength / 2 + i] = x[i] }
        let nBins = fftLength / 2 + 1
        guard let dft = vDSP_DFT_zop_CreateSetup(nil, vDSP_Length(fftLength), .FORWARD) else { return ([], 0) }
        defer { vDSP_DFT_DestroySetup(dft) }
        var inRe = [Float](repeating: 0, count: fftLength), inIm = [Float](repeating: 0, count: fftLength)
        var outRe = [Float](repeating: 0, count: fftLength), outIm = [Float](repeating: 0, count: fftLength)
        var mag = [Double](repeating: 0, count: m * nBins)
        for t in 0..<m {
            let s = t * hopLength
            for i in 0..<frameLength { inRe[i] = xp[s + i] * hannWindow[i] }
            for i in frameLength..<fftLength { inRe[i] = 0 }
            vDSP_DFT_Execute(dft, inRe, inIm, &outRe, &outIm)
            for b in 0..<nBins {
                let re = outRe[b], im = outIm[b]
                mag[t * nBins + b] = Double((re * re + im * im).squareRoot())   // magnitude in Float, like numpy complex64
            }
        }
        var mel = [Double](repeating: 0, count: m * nMels)
        melFilters.withUnsafeBufferPointer { f in
            mag.withUnsafeBufferPointer { a in
                mel.withUnsafeMutableBufferPointer { c in
                    // [m x 257] x [257 x 128] in double precision (row-major)
                    vDSP_mmulD(a.baseAddress!, 1, f.baseAddress!, 1, c.baseAddress!, 1, vDSP_Length(m), vDSP_Length(nMels), vDSP_Length(nBins))
                }
            }
        }
        var feats = [Float](repeating: 0, count: m * nMels)
        for i in 0..<(m * nMels) { feats[i] = Float(log(mel[i] + melFloor)) }
        return (feats, m)
    }

    // MARK: - Model inputs

    /// 16 kHz mono samples -> inputs of `audio_<bucket>` (first 30 s). Fewer than 161 samples give 0 tokens.
    public static func prepare(wav16k: [Float]) throws -> AudioInputs {
        let (f, nv) = logMel(wav16k)
        let Tb = try bucket(forFrames: max(nv, 1))
        var features = [Float16](repeating: 0, count: Tb * nMels)
        for i in 0..<(nv * nMels) { features[i] = Float16(f[i]) }
        var frameMask = [Float16](repeating: 0, count: Tb)
        for i in 0..<nv { frameMask[i] = 1 }
        var halfMask = [Float16](repeating: 0, count: Tb / 2)
        for i in 0..<((nv + 1) / 2) { halfMask[i] = 1 }
        let nt = softTokenCount(frames: nv)
        var tokenMask = [Float16](repeating: 0, count: Tb / 4)
        for i in 0..<nt { tokenMask[i] = 1 }
        return AudioInputs(bucket: Tb, frameCount: nv, softTokenCount: nt, features: features, frameMask: frameMask,
                           halfMask: halfMask, tokenMask: tokenMask)
    }

    /// Decode a file and prepare the inputs (decode -> mono -> 16 kHz -> log-mel).
    public static func prepare(url: URL) throws -> AudioInputs {
        try prepare(wav16k: try loadMono16k(url: url))
    }

    /// Consecutive 30 s windows (the last one shorter) for the optional chunk-mean embedding of long audio. A trailing
    /// window too short for one frame (< 161 samples, 0 tokens) is dropped unless it is the only one.
    public static func chunks30s(_ wav16k: [Float]) -> [[Float]] {
        if wav16k.isEmpty { return [[]] }
        let parts = stride(from: 0, to: wav16k.count, by: maxSamples).map { Array(wav16k[$0..<min($0 + maxSamples, wav16k.count)]) }
        return parts.enumerated().filter { $0.offset == 0 || $0.element.count > frameLength / 2 }.map { $0.element }
    }
}