char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

Xuan-Son Nguyenmtmd: propagate const to preproc class (#28310)de8656bd9

char/aliceai-t5-moe
57.7 KiB1557 linesraw
1#include "mtmd-audio.h"
2
3#define _USE_MATH_DEFINES // for M_PI
4#include <cmath>
5#include <cstdint>
6#include <cstring>
7#include <thread>
8#include <vector>
9#include <fstream>
10#include <algorithm>
11#include <functional>
12
13// some of the code here is copied from whisper.cpp
14
15constexpr bool DEBUG = false;
16
17void mtmd_audio_cache::fill_sin_cos_table(uint32_t n) {
18    sin_vals.resize(n);
19    cos_vals.resize(n);
20    for (uint32_t i = 0; i < n; i++) {
21        double theta = (2 * M_PI * i) / n;
22        sin_vals[i]  = sinf(theta);
23        cos_vals[i]  = cosf(theta);
24    }
25}
26
27void mtmd_audio_cache::fill_hann_window(uint32_t length, bool periodic) {
28    hann_window.resize(length);
29    int offset = periodic ? 0 : -1;
30    for (uint32_t i = 0; i < length; i++) {
31        hann_window[i] = 0.5 * (1.0 - cosf((2.0 * M_PI * i) / (length + offset)));
32    }
33}
34
35void mtmd_audio_cache::fill_mel_filterbank_matrix(int64_t n_mel,
36                                                  int64_t n_fft,
37                                                  int   sample_rate,
38                                                  float fmin,
39                                                  float fmax,
40                                                  bool  slaney_area_norm,
41                                                  float scale,
42                                                  bool  use_htk) {
43    GGML_ASSERT(n_mel > 0 && n_fft > 1);
44    if (fmax <= 0.0f) {
45        fmax = 0.5f * sample_rate;
46    }
47
48    std::function<double(double)> hz_to_mel;
49    std::function<double(double)> mel_to_hz;
50
51    if (use_htk) {
52        hz_to_mel = [](const double f_hz) -> double {
53            return 2595.0 * log10(1.0 + f_hz / 700.0);
54        };
55        mel_to_hz = [](const double m) -> double {
56            return 700.0 * (pow(10.0, m / 2595.0) - 1.0);
57        };
58    } else {
59        // Slaney scale (matches librosa default)
60        const double min_log_hz  = 1000.0;
61        const double lin_slope   = 3 / 200.;
62        const double min_log_mel = min_log_hz * lin_slope;
63        const double log_step    = log(6.4) / 27.0;
64        hz_to_mel = [min_log_hz, lin_slope, log_step, min_log_mel](const double f_hz) -> double {
65            return (f_hz < min_log_hz) ? f_hz * lin_slope : min_log_mel + log(f_hz / min_log_hz) / log_step;
66        };
67        mel_to_hz = [min_log_hz, lin_slope, log_step, min_log_mel](const double m) -> double {
68            return (m < min_log_mel) ? m / lin_slope : min_log_hz * exp((m - min_log_mel) * log_step);
69        };
70    }
71
72    // infer N_fft from n_fft_bins
73    const double bin_hz_step = double(sample_rate) / double(n_fft);
74
75    // mel grid: n_mel + 2 edges
76    const double        m_lo = hz_to_mel(fmin);
77    const double        m_hi = hz_to_mel(fmax);
78    std::vector<double> mel_pts(n_mel + 2);
79    for (int i = 0; i < n_mel + 2; ++i) {
80        mel_pts[i] = m_lo + (m_hi - m_lo) * (double(i) / (n_mel + 1));
81    }
82
83    // convert to Hz
84    std::vector<double> hz_pts(n_mel + 2);
85    for (int i = 0; i < n_mel + 2; ++i) {
86        hz_pts[i] = mel_to_hz(mel_pts[i]);
87    }
88
89    const int64_t n_fft_bins = n_fft / 2 + 1;
90
91    // Validate allocation size
92    if ((size_t)n_mel * (size_t)n_fft_bins > SIZE_MAX) {
93        GGML_ASSERT(false && "mel filterbank allocation too large");
94    }
95
96    // filterbank
97    std::vector<float> out((size_t)n_mel * (size_t)n_fft_bins, 0);
98    for (int64_t m = 0; m < n_mel; ++m) {
99        const double f_left   = hz_pts[m];
100        const double f_center = hz_pts[m + 1];
101        const double f_right  = hz_pts[m + 2];
102
103        const double denom_l = std::max(1e-30, f_center - f_left);
104        const double denom_r = std::max(1e-30, f_right - f_center);
105        const double enorm   = slaney_area_norm ? (2.0 / std::max(1e-30, f_right - f_left)) : 1.0;
106
107        for (int k = 0; k < n_fft_bins; ++k) {
108            const double f = k * bin_hz_step;
109            double       w = 0.0;
110            if (f >= f_left && f <= f_center) {
111                w = (f - f_left) / denom_l;
112            } else if (f > f_center && f <= f_right) {
113                w = (f_right - f) / denom_r;
114            }
115            out[size_t(m) * size_t(n_fft_bins) + size_t(k)] = float(w * enorm * scale);
116        }
117    }
118
119    filters.n_mel = n_mel;
120    filters.n_fft = n_fft;
121    filters.data  = std::move(out);
122
123    if (DEBUG) {  // debug
124        for (size_t i = 0; i < filters.data.size(); ++i) {
125            if (filters.data[i] != 0.0f) {
126                printf("filters[%zu] = %f\n", i, filters.data[i] * 1000.0f);
127            }
128        }
129    }
130}
131
132// Unified DFT implementation for both forward and inverse transforms
133// Template parameters:
134//   Inverse: false = DFT with exp(-2πi·k·n/N), no scaling
135//            true  = IDFT with exp(+2πi·k·n/N), scales by 1/N
136//   RealInput: true = input is real-valued (stride 1), avoids imaginary computations
137//              false = input is complex-valued (interleaved real/imag, stride 2)
138template <bool Inverse, bool RealInput>
139static void dft_impl(const mtmd_audio_cache & cache, const float * in, int N, float * out) {
140    const int n_sin_cos_vals = cache.sin_vals.size();
141    const int sin_cos_step   = n_sin_cos_vals / N;
142
143    constexpr float sign  = Inverse ? 1.0f : -1.0f;
144    const float     scale = Inverse ? (1.0f / N) : 1.0f;
145
146    for (int k = 0; k < N; k++) {
147        float re = 0;
148        float im = 0;
149
150        for (int n = 0; n < N; n++) {
151            int   idx     = (k * n * sin_cos_step) % n_sin_cos_vals;
152            float cos_val = cache.cos_vals[idx];
153            float sin_val = cache.sin_vals[idx];
154
155            if constexpr (RealInput) {
156                // Real input: in_im = 0, simplifies to:
157                // re += in_re * cos_val
158                // im += sign * in_re * sin_val
159                float in_re = in[n];
160                re += in_re * cos_val;
161                im += sign * in_re * sin_val;
162            } else {
163                float in_re = in[n * 2 + 0];
164                float in_im = in[n * 2 + 1];
165                // (a + bi) * (cos + sign*i*sin) = (a*cos - sign*b*sin) + (sign*a*sin + b*cos)i
166                re += in_re * cos_val - sign * in_im * sin_val;
167                im += sign * in_re * sin_val + in_im * cos_val;
168            }
169        }
170
171        out[k * 2 + 0] = re * scale;
172        out[k * 2 + 1] = im * scale;
173    }
174}
175
176// Cooley-Tukey FFT/IFFT unified implementation
177// Template parameters:
178//   Inverse: false = FFT with exp(-2πi·k/N), no scaling
179//            true  = IFFT with exp(+2πi·k/N), scales by 0.5 at each level
180//   RealInput: true = input is real-valued (stride 1)
181//              false = input is complex-valued (interleaved real/imag, stride 2)
182template <bool Inverse, bool RealInput>
183static void fft_impl(const mtmd_audio_cache & cache, float * in, int N, float * out) {
184    GGML_ASSERT(N > 0);
185    const int n_sin_cos_vals = cache.sin_vals.size();
186
187    if (N == 1) {
188        out[0] = in[0];
189        if constexpr (RealInput) {
190            out[1] = 0.0f;
191        } else {
192            out[1] = in[1];
193        }
194        return;
195    }
196
197    const int half_N = N / 2;
198    if (N - half_N * 2 == 1) {
199        // Odd N: fall back to DFT
200        dft_impl<Inverse, RealInput>(cache, in, N, out);
201        return;
202    }
203
204    // Split into even and odd
205    if constexpr (RealInput) {
206        // Real input: stride is 1, copy only real values
207        float * even = in + N;
208        for (int i = 0; i < half_N; ++i) {
209            even[i] = in[2 * i];
210        }
211        float * even_fft = out + 2 * N;
212        fft_impl<Inverse, true>(cache, even, half_N, even_fft);
213
214        float * odd = even;
215        for (int i = 0; i < half_N; ++i) {
216            odd[i] = in[2 * i + 1];
217        }
218        float * odd_fft = even_fft + N;
219        fft_impl<Inverse, true>(cache, odd, half_N, odd_fft);
220    } else {
221        // Complex input: stride is 2, copy complex pairs
222        float * even = in + N * 2;
223        for (int i = 0; i < half_N; ++i) {
224            even[i * 2 + 0] = in[2 * i * 2 + 0];
225            even[i * 2 + 1] = in[2 * i * 2 + 1];
226        }
227        float * even_fft = out + 2 * N;
228        fft_impl<Inverse, false>(cache, even, half_N, even_fft);
229
230        float * odd = even;
231        for (int i = 0; i < half_N; ++i) {
232            odd[i * 2 + 0] = in[(2 * i + 1) * 2 + 0];
233            odd[i * 2 + 1] = in[(2 * i + 1) * 2 + 1];
234        }
235        float * odd_fft = even_fft + N;
236        fft_impl<Inverse, false>(cache, odd, half_N, odd_fft);
237    }
238
239    float * even_fft = out + 2 * N;
240    float * odd_fft  = even_fft + N;
241
242    const int sin_cos_step = n_sin_cos_vals / N;
243
244    constexpr float sign  = Inverse ? 1.0f : -1.0f;
245    constexpr float scale = Inverse ? 0.5f : 1.0f;
246
247    for (int k = 0; k < half_N; k++) {
248        int   idx = k * sin_cos_step;  // t = 2*M_PI*k/N
249        float re  = cache.cos_vals[idx];
250        float im  = sign * cache.sin_vals[idx];
251
252        float re_odd = odd_fft[2 * k + 0];
253        float im_odd = odd_fft[2 * k + 1];
254
255        out[2 * k + 0] = scale * (even_fft[2 * k + 0] + re * re_odd - im * im_odd);
256        out[2 * k + 1] = scale * (even_fft[2 * k + 1] + re * im_odd + im * re_odd);
257
258        out[2 * (k + half_N) + 0] = scale * (even_fft[2 * k + 0] - re * re_odd + im * im_odd);
259        out[2 * (k + half_N) + 1] = scale * (even_fft[2 * k + 1] - re * im_odd - im * re_odd);
260    }
261}
262
263// Forward FFT for real input (used by mel spectrogram)
264static void fft(const mtmd_audio_cache & cache, float * in, int N, float * out) {
265    fft_impl<false, true>(cache, in, N, out);
266}
267
268// Inverse FFT for complex input
269static void ifft(const mtmd_audio_cache & cache, float * in, int N, float * out) {
270    fft_impl<true, false>(cache, in, N, out);
271}
272
273struct filter_params {
274    int64_t n_mel;
275    int64_t n_fft_bins;
276    int32_t hann_window_size;
277    int32_t hop_length;
278    int32_t sample_rate;
279    bool    no_padding      = false;
280    bool    center_padding  = false;
281    float   preemph         = 0.f;
282    bool    use_natural_log = false;
283    bool    norm_per_feature = false;
284    bool    use_magnitude   = false;  // |X| instead of |X|^2
285    float   mel_floor       = 5.960464477539063e-08f;
286};
287
288static void log_mel_spectrogram_worker_thread(int                        ith,
289                                              const float *              hann,
290                                              const std::vector<float> & samples,
291                                              int                        n_samples,
292                                              int                        frame_size,
293                                              int                        frame_step,
294                                              int                        n_threads,
295                                              const filter_params &      params,
296                                              const mtmd_audio_cache &   cache,
297                                              mtmd_audio_mel &           out) {
298    std::vector<float> fft_in(frame_size * 2, 0.0);
299    std::vector<float> fft_out(frame_size * 2 * 2 * 2);
300
301    int64_t n_fft_bins = params.n_fft_bins;
302    int64_t i = ith;
303
304    const auto & filters = cache.filters;
305
306    // make sure n_fft == 1 + (WHISPER_N_FFT / 2), bin_0 to bin_nyquist
307    GGML_ASSERT(n_fft_bins == 1 + (frame_size / 2));
308    GGML_ASSERT(cache.sin_vals.size() == cache.cos_vals.size());
309    // calculate FFT only when fft_in are not all zero
310    for (; i < std::min((int64_t)(n_samples / frame_step + 1), out.n_len); i += n_threads) {
311        const int64_t offset = i * frame_step;
312
313        // apply Hann window (~10% faster)
314        const int valid_len = std::min(frame_size, std::max(0, n_samples - (int)offset));
315        for (int j = 0; j < valid_len; j++) {
316            fft_in[j] = hann[j] * samples[offset + j];
317        }
318
319        // fill the rest with zeros
320        if (valid_len < frame_size) {
321            std::fill(fft_in.begin() + valid_len, fft_in.end(), 0.0);
322        }
323
324        // FFT
325        fft(cache, fft_in.data(), frame_size, fft_out.data());
326
327        // Calculate modulus^2 (power) or modulus (magnitude)
328        for (int j = 0; j < n_fft_bins; j++) {
329            float power = (fft_out[2 * j + 0] * fft_out[2 * j + 0] + fft_out[2 * j + 1] * fft_out[2 * j + 1]);
330            fft_out[j] = params.use_magnitude ? sqrtf(power) : power;
331        }
332
333        // mel spectrogram
334        for (int64_t j = 0; j < out.n_mel; j++) {
335            double sum = 0.0;
336            // unroll loop (suggested by GH user @lunixbochs)
337            int k = 0;
338            for (k = 0; k < n_fft_bins - 3; k += 4) {
339                size_t idx = size_t(j) * size_t(n_fft_bins) + size_t(k);
340                sum +=
341                        fft_out[k + 0] * filters.data[idx + 0] +
342                        fft_out[k + 1] * filters.data[idx + 1] +
343                        fft_out[k + 2] * filters.data[idx + 2] +
344                        fft_out[k + 3] * filters.data[idx + 3];
345            }
346            // handle n_fft remainder
347            for (; k < n_fft_bins; k++) {
348                sum += fft_out[k] * filters.data[(size_t)j * n_fft_bins + k];
349            }
350            sum = std::max(sum, (double)params.mel_floor);
351            sum = params.use_natural_log
352                ? log(sum)
353                : log10(sum);
354            out.data[(size_t)j * out.n_len + i] = sum;
355        }
356    }
357
358    // Otherwise fft_out are all zero
359    double sum = params.use_natural_log ? log(1e-10) : log10(1e-10);
360    for (; i < out.n_len; i += n_threads) {
361        for (int64_t j = 0; j < out.n_mel; j++) {
362            out.data[(size_t)j * out.n_len + i] = sum;
363        }
364    }
365}
366
367// ref: https://github.com/openai/whisper/blob/main/whisper/audio.py#L110-L157
368static bool log_mel_spectrogram(
369        const float * samples,
370        const int     n_samples_in,
371        const int     n_threads,
372        const filter_params & params,
373        const mtmd_audio_cache & cache,
374        mtmd_audio_mel & out) {
375    //const int64_t t_start_us = ggml_time_us();
376
377    out.n_len_org = n_samples_in;
378    int n_samples = n_samples_in;
379
380    // Hann window
381    const float * hann       = cache.hann_window.data();
382    const int     frame_size = (params.n_fft_bins - 1) * 2;
383    const int     frame_step = params.hop_length;
384
385    // Padding
386    std::vector<float> samples_padded;
387    if (params.no_padding) {
388        // no padding, use samples as-is
389        samples_padded = std::vector<float>(samples, samples + n_samples);
390        samples = samples_padded.data();
391        n_samples = samples_padded.size();
392    } else if (params.center_padding) {
393        const auto pad_amount = frame_size / 2;
394        samples_padded = std::vector<float>(n_samples + 2 * pad_amount, 0);
395        std::copy(samples, samples + n_samples, samples_padded.data() + pad_amount);
396        samples = samples_padded.data();
397        n_samples = samples_padded.size();
398    } else {
399        // existing padding logic
400        int64_t stage_1_pad = params.sample_rate * 30;
401        int64_t stage_2_pad = frame_size / 2;
402        samples_padded.resize(n_samples + stage_1_pad + stage_2_pad * 2);
403        std::copy(samples, samples + n_samples, samples_padded.begin() + stage_2_pad);
404        // pad 30 seconds of zeros at the end of audio (480,000 samples) + reflective pad 200 samples at the end of audio
405        std::fill(samples_padded.begin() + n_samples + stage_2_pad, samples_padded.begin() + n_samples + stage_1_pad + 2 * stage_2_pad, 0);
406        // reflective pad 200 samples at the beginning of audio
407        if (n_samples < stage_2_pad + 1) {
408            // TODO: Handle short audio differently or return error
409            return false;
410        }
411        std::reverse_copy(samples + 1, samples + 1 + stage_2_pad, samples_padded.begin());
412
413        // expose the padded buffer to downstream FFT and to out.n_len computation
414        // mirrors the no_padding and center_padding branches above
415        samples   = samples_padded.data();
416        n_samples = samples_padded.size();
417    }
418
419    // preemphasis
420    if (params.preemph) {
421        const int   pad_amount = frame_size / 2;
422        const float preemph = 0.97f;
423        float       prev = samples_padded[pad_amount];
424        for (int i = pad_amount + 1; i + pad_amount < n_samples; ++i) {
425            float cur = samples_padded[i];
426            samples_padded[i] = cur - preemph * prev;
427            prev = cur;
428        }
429    }
430
431    // pad hann window if it's smaller than frame_size
432    // TODO: probably unnecessary here? (or better doing it in g_cache?)
433    std::vector<float> hann_window_padded;
434    if (params.hann_window_size < frame_size) {
435        hann_window_padded.resize(frame_size);
436        const int padding = (frame_size - params.hann_window_size) / 2;
437        std::copy(hann, hann + params.hann_window_size, &hann_window_padded[padding]);
438        hann = hann_window_padded.data();
439    }
440
441
442    GGML_ASSERT(params.n_fft_bins > 0);
443    GGML_ASSERT(params.hop_length > 0);
444    out.n_mel = params.n_mel;
445    out.n_len = (n_samples - frame_size) / frame_step + 1;
446    // Validate dimensions before allocation to prevent integer overflow
447    if (out.n_mel <= 0 || out.n_len <= 0) {
448        LOG_ERR("%s: invalid mel dimensions n_mel=%lld n_len=%lld\n", __func__, (long long)out.n_mel, (long long)out.n_len);
449        return false;
450    }
451    const size_t total_size = (size_t)out.n_mel * (size_t)out.n_len;
452    if (total_size > SIZE_MAX / sizeof(float)) {
453        LOG_ERR("%s: size overflow: n_mel=%lld n_len=%lld\n", __func__, (long long)out.n_mel, (long long)out.n_len);
454        return false;
455    }
456    if (n_samples < frame_size) {
457        LOG_ERR("%s: not enough samples after padding\n", __func__);
458        return false;
459    }
460    out.data.resize(total_size);
461
462    {
463        std::vector<std::thread> workers(n_threads - 1);
464        for (int iw = 0; iw < n_threads - 1; ++iw) {
465            workers[iw] =
466                std::thread(log_mel_spectrogram_worker_thread, iw + 1, hann, std::cref(samples_padded), n_samples,
467                            frame_size, frame_step, n_threads, std::cref(params), std::cref(cache), std::ref(out));
468        }
469
470        // main thread
471        log_mel_spectrogram_worker_thread(0, hann, samples_padded, n_samples, frame_size, frame_step, n_threads, params,
472                                          cache, out);
473        for (int iw = 0; iw < n_threads - 1; ++iw) {
474            workers[iw].join();
475        }
476    }
477
478    const int64_t effective_n_len = n_samples_in / frame_step;
479    if (params.norm_per_feature) {
480        GGML_ASSERT(effective_n_len > 1);
481        for (int64_t i = 0; i < out.n_mel; i++) {
482            double mean = 0;
483            for (int64_t j = 0; j < effective_n_len; ++j) {
484                mean += out.data[(size_t)i * out.n_len + j];
485            }
486            mean /= effective_n_len;
487
488            double var = 0.0;
489            for (int64_t j = 0; j < effective_n_len; ++j) {
490                const double value = out.data[(size_t)i * out.n_len + j] - mean;
491                var += value * value;
492            }
493            var /= effective_n_len - 1;  // unbiased
494            const double mstd = std::sqrt(var + 1e-5);
495
496            for (int64_t j = 0; j < effective_n_len; ++j) {
497                auto &value = out.data[(size_t)i * out.n_len + j];
498                value        = (value - mean) / mstd;
499            }
500
501            // pad the rest with zeros
502            for (int64_t j = effective_n_len; j < out.n_len; ++j) {
503                out.data[(size_t)i * out.n_len + j] = 0.0;
504            }
505        }
506    } else if (!params.no_padding) {
507        // Whisper-style clamping and normalization (NOT used by Gemma4)
508        double mmax = -1e20;
509        const size_t mel_size = (size_t)out.n_mel * (size_t)out.n_len;
510        for (size_t i = 0; i < mel_size; i++) {
511            if (out.data[i] > mmax) {
512                mmax = out.data[i];
513            }
514        }
515
516        mmax -= 8.0;
517
518        for (size_t i = 0; i < mel_size; i++) {
519            if (out.data[i] < mmax) {
520                out.data[i] = mmax;
521            }
522            out.data[i] = (out.data[i] + 4.0)/4.0;
523        }
524    }
525
526    // Dump log_mel_spectrogram
527    if (DEBUG) {
528        std::ofstream outFile("log_mel_spectrogram.json");
529        outFile << "[";
530        for (uint64_t i = 0; i < out.data.size() - 1; i++) {
531            outFile << out.data[i] << ", ";
532        }
533        outFile << out.data[out.data.size() - 1] << "]";
534        outFile.close();
535    }
536
537    return true;
538}
539
540//
541// mtmd_audio_preprocessor_whisper
542//
543
544void mtmd_audio_preprocessor_whisper::initialize() {
545    cache.fill_sin_cos_table(hparams.audio_n_fft);
546    cache.fill_hann_window(hparams.audio_window_len, true);
547    cache.fill_mel_filterbank_matrix(hparams.n_mel_bins, hparams.audio_n_fft, hparams.audio_sample_rate);
548}
549
550bool mtmd_audio_preprocessor_whisper::preprocess(const float *                 samples,
551                                                 size_t                        n_samples,
552                                                 std::vector<mtmd_audio_mel> & output) const {
553    if (n_samples == 0) {
554        // empty audio
555        return false;
556    }
557
558    std::vector<float> smpl;
559    // reflection padding needs one sample plus half an FFT window
560    size_t min_samples = (size_t) hparams.audio_n_fft / 2 + 1;
561    if (n_samples < min_samples) {
562        smpl.resize(min_samples, 0.0f);
563        std::memcpy(smpl.data(), samples, n_samples * sizeof(float));
564        samples   = smpl.data();
565        n_samples = smpl.size();
566    }
567
568    filter_params params;
569    params.n_mel            = hparams.n_mel_bins;
570    params.n_fft_bins       = 1 + (hparams.audio_n_fft / 2);
571    params.hann_window_size = hparams.audio_window_len;
572    params.hop_length       = hparams.audio_hop_len;
573    params.sample_rate      = hparams.audio_sample_rate;
574    params.center_padding   = false;
575    params.preemph          = 0.0f;  // disabled
576    params.use_natural_log  = false;
577    params.norm_per_feature = false;
578
579    // make sure the cache is initialized
580    GGML_ASSERT(!cache.sin_vals.empty());
581    GGML_ASSERT(!cache.cos_vals.empty());
582    GGML_ASSERT(!cache.filters.data.empty());
583
584    mtmd_audio_mel out_full;
585    bool           ok = log_mel_spectrogram(samples, n_samples,
586                                            4,  // n_threads
587                                            params, cache, out_full);
588    if (!ok) {
589        return false;
590    }
591
592    // because the cgraph in clip.cpp only accepts 3000 frames each, we need to split the mel
593    // we always expect the mel to have 3000 silent frames at the end
594    if (DEBUG) {
595        printf("output: n_mel = %d, n_len = %d\n", (int) out_full.n_mel, (int) out_full.n_len);
596    }
597    const size_t frames_per_chunk = 3000;
598    GGML_ASSERT((size_t) out_full.n_len > frames_per_chunk);
599    for (size_t off = 0; off < (size_t) out_full.n_len; off += frames_per_chunk) {
600        int64_t n_len = std::min((int64_t)frames_per_chunk, out_full.n_len - (int64_t)off);
601        if (n_len < (int64_t)frames_per_chunk) {
602            break;  // last incomplete chunk will always be a padded chunk, safe to ignore
603        }
604
605        mtmd_audio_mel out_chunk;
606        out_chunk.n_len     = n_len;
607        out_chunk.n_mel     = out_full.n_mel;
608        out_chunk.n_len_org = out_full.n_mel;  // unused
609        out_chunk.data.reserve((size_t)out_chunk.n_mel * (size_t)out_chunk.n_len);
610
611        for (int64_t i = 0; i < out_full.n_mel; i++) {
612            auto src = out_full.data.begin() + (size_t)i * out_full.n_len + off;
613            out_chunk.data.insert(out_chunk.data.end(), src, src + frames_per_chunk);
614        }
615
616        output.push_back(std::move(out_chunk));
617    }
618
619    return true;
620}
621
622//
623// mtmd_audio_preprocessor_qwen3a
624//
625// Matches the Python WhisperFeatureExtractor called with truncation=False:
626//   - reflection padding of n_fft/2 samples at each end (center=True)
627//   - Whisper-style log10 + (max-8)/4 normalization applied to full audio
628//   - output split into ≤30s (3000 mel frames) windows, each padded to a
629//     multiple of 200 frames (n_window * 2) for the cgraph batch view
630//
631
632void mtmd_audio_preprocessor_qwen3a::initialize() {
633    cache.fill_sin_cos_table(hparams.audio_n_fft);
634    cache.fill_hann_window(hparams.audio_window_len, true);
635    cache.fill_mel_filterbank_matrix(hparams.n_mel_bins, hparams.audio_n_fft, hparams.audio_sample_rate);
636}
637
638bool mtmd_audio_preprocessor_qwen3a::preprocess(const float *                 samples,
639                                                 size_t                        n_samples,
640                                                 std::vector<mtmd_audio_mel> & output) const {
641    if (n_samples == 0) {
642        return false;
643    }
644
645    GGML_ASSERT(!cache.sin_vals.empty());
646    GGML_ASSERT(!cache.cos_vals.empty());
647    GGML_ASSERT(!cache.filters.data.empty());
648
649    // Reflection-pad n_fft/2 samples at each end, matching WhisperFeatureExtractor center=True
650    const int pad = hparams.audio_n_fft / 2; // = 200
651
652    std::vector<float> padded(n_samples + 2 * pad, 0.0f);
653    // Reflect start: padded[0..pad-1] = samples[pad..1] (reversed)
654    for (int i = 0; i < pad; i++) {
655        int src = pad - i; // samples[pad], samples[pad-1], ..., samples[1]
656        padded[i] = (src < (int)n_samples) ? samples[src] : 0.0f;
657    }
658    std::copy(samples, samples + n_samples, padded.begin() + pad);
659    // Reflect end: padded[n+pad..n+2*pad-1] = samples[n-2..n-pad-1] (reversed)
660    for (int i = 0; i < pad; i++) {
661        int src = (int)n_samples - 2 - i; // samples[n-2], samples[n-3], ...
662        padded[n_samples + pad + i] = (src >= 0) ? samples[src] : 0.0f;
663    }
664
665    filter_params params;
666    params.n_mel            = hparams.n_mel_bins;
667    params.n_fft_bins       = 1 + (hparams.audio_n_fft / 2);
668    params.hann_window_size = hparams.audio_window_len;
669    params.hop_length       = hparams.audio_hop_len;
670    params.sample_rate      = hparams.audio_sample_rate;
671    params.no_padding       = true; // reflection padding already applied above
672    params.use_natural_log  = false; // log10
673
674    mtmd_audio_mel mel_full;
675    bool ok = log_mel_spectrogram(padded.data(), (int)padded.size(), 4, params, cache, mel_full);
676    if (!ok) {
677        return false;
678    }
679
680    // Whisper-style normalization: clamp to (max - 8), scale to [-1, 1]
681    {
682        double mmax = -1e20;
683        for (float v : mel_full.data) {
684            if (v > mmax) mmax = v;
685        }
686        mmax -= 8.0;
687        for (float & v : mel_full.data) {
688            v = (std::max((double)v, mmax) + 4.0) / 4.0;
689        }
690    }
691
692    // The effective frame count: center-padded STFT gives ~n_samples/hop_length frames.
693    // We take min(mel_full.n_len, n_samples/hop + 1) to avoid including excess frames.
694    const int64_t n_eff = std::min(mel_full.n_len,
695                               (int64_t)(n_samples / hparams.audio_hop_len) + 1);
696
697    // Split into inference windows matching n_window_infer=800 from model config.
698    // Each window is padded to the next multiple of chunk_size for the cgraph.
699    // The mtmd caller loops over output entries, so long audio is handled automatically.
700    const int chunk_size  = 100; // conv sub-chunk size (n_window * 2, n_window=50)
701    const int window_size = 800; // mel frames per forward pass (n_window_infer=800)
702
703    for (int64_t off = 0; off < n_eff; off += window_size) {
704        const int64_t win_eff  = std::min((int64_t)window_size, n_eff - off);
705        const int64_t n_chunks  = (win_eff + chunk_size - 1) / chunk_size;
706        const int64_t n_padded  = n_chunks * chunk_size;
707
708        mtmd_audio_mel out;
709        out.n_mel     = mel_full.n_mel;
710        out.n_len     = n_padded;
711        out.n_len_org = win_eff;
712        out.data.assign((size_t)out.n_mel * (size_t)out.n_len, 0.0f);
713        for (int64_t m = 0; m < out.n_mel; m++) {
714            const int64_t copy_len = std::min((int64_t)win_eff, mel_full.n_len - off);
715            if (copy_len > 0) {
716                std::copy(mel_full.data.begin() + (size_t)m * mel_full.n_len + off,
717                          mel_full.data.begin() + (size_t)m * mel_full.n_len + off + copy_len,
718                          out.data.begin()      + (size_t)m * out.n_len);
719            }
720        }
721        output.push_back(std::move(out));
722    }
723    return true;
724}
725
726//
727// mtmd_audio_preprocessor_dots3note
728//
729// Matches Dots3NoteFeatureExtractor: the waveform is split into 60s chunks and each chunk gets
730// its own whisper-style log-mel (center=True, log10 + (max-8)/4). Only sample_length//hop frames
731// per chunk are valid; the reference masks everything beyond them, so we emit exactly that many.
732//
733
734void mtmd_audio_preprocessor_dots3note::initialize() {
735    cache.fill_sin_cos_table(hparams.audio_n_fft);
736    cache.fill_hann_window(hparams.audio_window_len, true);
737    cache.fill_mel_filterbank_matrix(hparams.n_mel_bins, hparams.audio_n_fft, hparams.audio_sample_rate);
738}
739
740bool mtmd_audio_preprocessor_dots3note::preprocess(const float *                 samples,
741                                                   size_t                        n_samples,
742                                                   std::vector<mtmd_audio_mel> & output) const {
743    if (n_samples == 0) {
744        return false;
745    }
746
747    GGML_ASSERT(!cache.sin_vals.empty());
748    GGML_ASSERT(!cache.cos_vals.empty());
749    GGML_ASSERT(!cache.filters.data.empty());
750
751    const int    pad           = hparams.audio_n_fft / 2; // center=True padding
752    const int    hop           = hparams.audio_hop_len;
753    const size_t chunk_samples = (size_t) hparams.audio_chunk_len * hparams.audio_sample_rate;
754
755    for (size_t start = 0; start < n_samples; start += chunk_samples) {
756        const size_t n_chunk = std::min(chunk_samples, n_samples - start);
757        const float * chunk  = samples + start;
758
759        const int64_t n_valid = n_chunk / hop;
760        if (n_valid == 0) {
761            continue; // sub-hop tail, contributes no frames
762        }
763
764        // reflect-pad the start; the reference zero-pads partial chunks to 60s before the STFT,
765        // so a partial chunk sees zeros past its end while a full chunk reflects its own tail
766        std::vector<float> padded(n_chunk + 2 * pad, 0.0f);
767        for (int i = 0; i < pad; i++) {
768            int src = pad - i;
769            padded[i] = (src < (int) n_chunk) ? chunk[src] : 0.0f;
770        }
771        std::copy(chunk, chunk + n_chunk, padded.begin() + pad);
772        if (n_chunk == chunk_samples) {
773            for (int i = 0; i < pad; i++) {
774                int src = (int) n_chunk - 2 - i;
775                padded[n_chunk + pad + i] = (src >= 0) ? chunk[src] : 0.0f;
776            }
777        }
778
779        filter_params params;
780        params.n_mel            = hparams.n_mel_bins;
781        params.n_fft_bins       = 1 + (hparams.audio_n_fft / 2);
782        params.hann_window_size = hparams.audio_window_len;
783        params.hop_length       = hop;
784        params.sample_rate      = hparams.audio_sample_rate;
785        params.no_padding       = true; // padding already applied above
786        params.use_natural_log  = false;
787
788        mtmd_audio_mel mel_full;
789        if (!log_mel_spectrogram(padded.data(), (int) padded.size(), 4, params, cache, mel_full)) {
790            return false;
791        }
792        GGML_ASSERT(mel_full.n_len >= n_valid);
793
794        // per-chunk whisper-style normalization, then keep only the valid frames
795        mtmd_audio_mel out;
796        out.n_mel     = mel_full.n_mel;
797        out.n_len     = n_valid;
798        out.n_len_org = n_valid;
799        out.data.resize((size_t) out.n_mel * (size_t) out.n_len);
800
801        double mmax = -1e20;
802        for (int64_t m = 0; m < out.n_mel; m++) {
803            for (int64_t t = 0; t < n_valid; t++) {
804                mmax = std::max(mmax, (double) mel_full.data[(size_t) m * mel_full.n_len + t]);
805            }
806        }
807        mmax -= 8.0;
808        for (int64_t m = 0; m < out.n_mel; m++) {
809            for (int64_t t = 0; t < n_valid; t++) {
810                const double v = std::max((double) mel_full.data[(size_t) m * mel_full.n_len + t], mmax);
811                out.data[(size_t) m * n_valid + t] = (float) ((v + 4.0) / 4.0);
812            }
813        }
814
815        output.push_back(std::move(out));
816    }
817    return !output.empty();
818}
819
820//
821// mtmd_audio_preprocessor_mimo_audio
822//
823// Matches torchaudio.transforms.MelSpectrogram(power=1.0, center=True) followed by
824// log(clip(spec, min=1e-7)): HTK mel scale, no Slaney area norm, magnitude (not power)
825// spectrogram, natural log, reflect-padded by n_fft/2 on each side.
826//
827
828void mtmd_audio_preprocessor_mimo_audio::initialize() {
829    cache.fill_sin_cos_table(hparams.audio_n_fft);
830    cache.fill_hann_window(hparams.audio_window_len, true);
831    cache.fill_mel_filterbank_matrix(
832        hparams.n_mel_bins, hparams.audio_n_fft, hparams.audio_sample_rate,
833        0.0f, hparams.audio_sample_rate / 2.0f,
834        /*slaney_area_norm=*/ false,
835        /*scale=*/ 1.0f,
836        /*use_htk=*/ true
837    );
838}
839
840bool mtmd_audio_preprocessor_mimo_audio::preprocess(const float *                 samples,
841                                                    size_t                        n_samples,
842                                                    std::vector<mtmd_audio_mel> & output) const {
843    if (n_samples == 0) {
844        return false;
845    }
846
847    GGML_ASSERT(!cache.sin_vals.empty());
848    GGML_ASSERT(!cache.cos_vals.empty());
849    GGML_ASSERT(!cache.filters.data.empty());
850
851    const int pad = hparams.audio_n_fft / 2;
852
853    std::vector<float> padded(n_samples + 2 * pad, 0.0f);
854    for (int i = 0; i < pad; i++) {
855        int src = pad - i;
856        padded[i] = (src < (int)n_samples) ? samples[src] : 0.0f;
857    }
858    std::copy(samples, samples + n_samples, padded.begin() + pad);
859    for (int i = 0; i < pad; i++) {
860        int src = (int)n_samples - 2 - i;
861        padded[n_samples + pad + i] = (src >= 0) ? samples[src] : 0.0f;
862    }
863
864    filter_params params;
865    params.n_mel            = hparams.n_mel_bins;
866    params.n_fft_bins       = 1 + (hparams.audio_n_fft / 2);
867    params.hann_window_size = hparams.audio_window_len;
868    params.hop_length       = hparams.audio_hop_len;
869    params.sample_rate      = hparams.audio_sample_rate;
870    params.no_padding       = true; // reflect padding already applied above
871    params.use_natural_log  = true;
872    params.use_magnitude    = true;
873    params.mel_floor        = 1e-7f;
874    params.norm_per_feature = false;
875
876    mtmd_audio_mel out;
877    bool ok = log_mel_spectrogram(padded.data(), (int)padded.size(), 4, params, cache, out);
878    if (!ok) {
879        return false;
880    }
881
882    output.push_back(std::move(out));
883    return true;
884}
885
886//
887// mtmd_audio_preprocessor_qwen3tts_spk
888//
889// same as mel_spectrogram() in modeling_qwen3_tts.py
890// ECAPA-TDNN takes the whole clip in one pass, so no Whisper-style chunking or normalization
891//
892
893void mtmd_audio_preprocessor_qwen3tts_spk::initialize() {
894    cache.fill_sin_cos_table(hparams.audio_n_fft);
895    cache.fill_hann_window(hparams.audio_window_len, true);
896    cache.fill_mel_filterbank_matrix(hparams.n_mel_bins, hparams.audio_n_fft, hparams.audio_sample_rate);
897}
898
899bool mtmd_audio_preprocessor_qwen3tts_spk::preprocess(const float *                 samples,
900                                                      size_t                        n_samples,
901                                                      std::vector<mtmd_audio_mel> & output) const {
902    if (n_samples == 0) {
903        return false;
904    }
905
906    GGML_ASSERT(!cache.sin_vals.empty());
907    GGML_ASSERT(!cache.cos_vals.empty());
908    GGML_ASSERT(!cache.filters.data.empty());
909
910    // reflect pad by (n_fft - hop) / 2 = 384, matching center=False STFT framing
911    const int pad = (hparams.audio_n_fft - hparams.audio_hop_len) / 2;
912    if (n_samples < (size_t) pad + 1) {
913        return false;
914    }
915
916    std::vector<float> padded(n_samples + 2 * pad, 0.0f);
917    for (int i = 0; i < pad; i++) {
918        padded[i] = samples[pad - i];
919    }
920    std::copy(samples, samples + n_samples, padded.begin() + pad);
921    for (int i = 0; i < pad; i++) {
922        padded[n_samples + pad + i] = samples[n_samples - 2 - i];
923    }
924
925    filter_params params;
926    params.n_mel            = hparams.n_mel_bins;
927    params.n_fft_bins       = 1 + (hparams.audio_n_fft / 2);
928    params.hann_window_size = hparams.audio_window_len;
929    params.hop_length       = hparams.audio_hop_len;
930    params.sample_rate      = hparams.audio_sample_rate;
931    params.no_padding       = true; // reflect padding already applied above
932    params.use_natural_log  = true;
933    params.use_magnitude    = true;
934    params.mel_floor        = 1e-5f;
935
936    mtmd_audio_mel out;
937    bool ok = log_mel_spectrogram(padded.data(), (int) padded.size(), 4, params, cache, out);
938    if (!ok) {
939        return false;
940    }
941
942    output.push_back(std::move(out));
943    return true;
944}
945
946//
947// mtmd_audio_preprocessor_conformer
948//
949
950void mtmd_audio_preprocessor_conformer::initialize() {
951    cache.fill_sin_cos_table(hparams.audio_n_fft);
952    cache.fill_hann_window(hparams.audio_window_len, true);
953    cache.fill_mel_filterbank_matrix(hparams.n_mel_bins, hparams.audio_n_fft, hparams.audio_sample_rate);
954}
955
956bool mtmd_audio_preprocessor_conformer::preprocess(const float *                 samples,
957                                                   size_t                        n_samples,
958                                                   std::vector<mtmd_audio_mel> & output) const {
959    // empty audio
960    if (n_samples == 0) {
961        return false;
962    }
963
964    filter_params params;
965    params.n_mel            = hparams.n_mel_bins;
966    params.n_fft_bins       = 1 + (hparams.audio_n_fft / 2);
967    params.hann_window_size = hparams.audio_window_len;
968    params.hop_length       = hparams.audio_hop_len;
969    params.sample_rate      = hparams.audio_sample_rate;
970    params.center_padding   = true;
971    params.preemph          = 0.97f;
972    params.use_natural_log  = true;
973    params.norm_per_feature = true;
974
975    // make sure the cache is initialized
976    GGML_ASSERT(!cache.sin_vals.empty());
977    GGML_ASSERT(!cache.cos_vals.empty());
978    GGML_ASSERT(!cache.filters.data.empty());
979
980    mtmd_audio_mel out_full;
981    bool           ok = log_mel_spectrogram(samples, n_samples,
982                                            4,  // n_threads
983                                            params, cache, out_full);
984    if (!ok) {
985        return false;
986    }
987
988    output.push_back(std::move(out_full));
989    return true;
990}
991
992//
993// mtmd_audio_preprocessor_granite_speech
994//
995
996void mtmd_audio_preprocessor_granite_speech::initialize() {
997    cache.fill_sin_cos_table(hparams.audio_n_fft);
998    cache.fill_hann_window(hparams.audio_window_len, true);
999    cache.fill_mel_filterbank_matrix(
1000        hparams.n_mel_bins / 2, hparams.audio_n_fft, hparams.audio_sample_rate,
1001        0.0f, -1.0f, false, 1.0f, true);
1002}
1003
1004bool mtmd_audio_preprocessor_granite_speech::preprocess(const float *                 samples,
1005                                                        size_t                        n_samples,
1006                                                        std::vector<mtmd_audio_mel> & output) const {
1007    if (n_samples == 0) {
1008        return false;
1009    }
1010
1011    GGML_ASSERT(!cache.sin_vals.empty());
1012    GGML_ASSERT(!cache.cos_vals.empty());
1013    GGML_ASSERT(!cache.filters.data.empty());
1014
1015    const int n_fft = hparams.audio_n_fft;
1016    const int pad   = n_fft / 2;
1017
1018    // reflect padding
1019    const int n_padded = (int)n_samples + 2 * pad;
1020    std::vector<float> padded(n_padded, 0.0f);
1021    std::copy(samples, samples + n_samples, padded.data() + pad);
1022    for (int i = 0; i < pad; i++) {
1023        int src = i + 1;
1024        if (src >= (int)n_samples) {
1025            src = (int)n_samples - 1;
1026        }
1027        padded[pad - 1 - i] = samples[src];
1028    }
1029    for (int i = 0; i < pad; i++) {
1030        int src = (int)n_samples - 2 - i;
1031        if (src < 0) {
1032            src = 0;
1033        }
1034        padded[pad + (int)n_samples + i] = samples[src];
1035    }
1036
1037    filter_params params;
1038    params.n_mel            = hparams.n_mel_bins / 2;
1039    params.n_fft_bins       = 1 + (n_fft / 2);
1040    params.hann_window_size = hparams.audio_window_len;
1041    params.hop_length       = hparams.audio_hop_len;
1042    params.sample_rate      = hparams.audio_sample_rate;
1043    params.no_padding       = true;
1044    params.center_padding   = false;
1045    params.preemph          = 0.0f;
1046    params.use_natural_log  = false;
1047    params.norm_per_feature = false;
1048    params.mel_floor        = 1e-10f;
1049
1050    mtmd_audio_mel mel;
1051    if (!log_mel_spectrogram(padded.data(), n_padded, 4, params, cache, mel)) {
1052        return false;
1053    }
1054
1055    double mmax = -1e20;
1056    const size_t mel_size = (size_t)mel.n_mel * (size_t)mel.n_len;
1057    for (size_t i = 0; i < mel_size; i++) {
1058        if (mel.data[i] > mmax) {
1059            mmax = mel.data[i];
1060        }
1061    }
1062    mmax -= 8.0;
1063
1064    for (size_t i = 0; i < mel_size; i++) {
1065        if (mel.data[i] < mmax) {
1066            mel.data[i] = mmax;
1067        }
1068        mel.data[i] = (mel.data[i] + 4.0) / 4.0;
1069    }
1070
1071    int64_t n_frames = mel.n_len;
1072    if (n_frames % 2 == 1) {
1073        n_frames--;
1074    }
1075    const int64_t n_mel     = mel.n_mel;
1076    const int64_t n_stacked = n_frames / 2;
1077
1078    mtmd_audio_mel stacked;
1079    stacked.n_mel     = 2 * n_mel;
1080    stacked.n_len     = n_stacked;
1081    stacked.n_len_org = (int64_t)n_samples;
1082    stacked.data.resize((size_t)2 * (size_t)n_mel * (size_t)n_stacked);
1083
1084    for (int64_t t = 0; t < n_stacked; t++) {
1085        for (int64_t m = 0; m < n_mel; m++) {
1086            stacked.data[(size_t)m * n_stacked + t] = mel.data[(size_t)m * mel.n_len + 2 * t];
1087            stacked.data[(size_t)(m + n_mel) * n_stacked + t] = mel.data[(size_t)m * mel.n_len + 2 * t + 1];
1088        }
1089    }
1090
1091    output.push_back(std::move(stacked));
1092    return true;
1093}
1094
1095//
1096// mtmd_audio_preprocessor_gemma4a
1097//
1098
1099void mtmd_audio_preprocessor_gemma4a::initialize() {
1100    cache.fill_sin_cos_table(hparams.audio_n_fft);
1101
1102    // Standard periodic Hann window, zero-padded to FFT size
1103    cache.hann_window.assign(hparams.audio_n_fft, 0.0f);
1104    for (uint32_t i = 0; i < (uint32_t)hparams.audio_window_len; i++) {
1105        cache.hann_window[i] = 0.5f - 0.5f * cosf((2.0f * (float)M_PI * i) / hparams.audio_window_len);
1106    }
1107
1108    // HTK mel scale, no Slaney area normalization
1109    cache.fill_mel_filterbank_matrix(
1110        hparams.n_mel_bins, hparams.audio_n_fft, hparams.audio_sample_rate,
1111        0.0f, hparams.audio_sample_rate / 2.0f,
1112        /*slaney_area_norm=*/ false,
1113        /*scale=*/ 1.0f,
1114        /*use_htk=*/ true
1115    );
1116}
1117
1118bool mtmd_audio_preprocessor_gemma4a::preprocess(const float *                 samples,
1119                                                  size_t                        n_samples,
1120                                                  std::vector<mtmd_audio_mel> & output) const {
1121    if (n_samples == 0) {
1122        return false;
1123    }
1124
1125    GGML_ASSERT(!cache.sin_vals.empty());
1126    GGML_ASSERT(!cache.cos_vals.empty());
1127    GGML_ASSERT(!cache.filters.data.empty());
1128
1129    filter_params params;
1130    params.n_mel            = hparams.n_mel_bins;
1131    params.n_fft_bins       = 1 + (hparams.audio_n_fft / 2);
1132    params.hann_window_size = hparams.audio_n_fft; // window is zero-padded to FFT size
1133    params.hop_length       = hparams.audio_hop_len;
1134    params.sample_rate      = hparams.audio_sample_rate;
1135    params.no_padding       = true;
1136    params.center_padding   = false;
1137    params.preemph          = 0.0f;
1138    params.use_natural_log  = true;
1139    params.use_magnitude    = true;
1140    params.mel_floor        = 0.001f;
1141    params.norm_per_feature = false;
1142
1143    // Split into 30-second chunks (model context limit, ~750 tokens each)
1144    const size_t chunk_samples = 30 * hparams.audio_sample_rate;
1145    for (size_t off = 0; off < n_samples; off += chunk_samples) {
1146        const float * chunk_ptr = samples + off;
1147        size_t chunk_len = std::min(chunk_samples, n_samples - off);
1148
1149        // Semicausal left-padding + right-padding to match PyTorch frame count
1150        const int pad_left = hparams.audio_window_len / 2;
1151        const int fft_size = hparams.audio_n_fft;
1152        const int hop = hparams.audio_hop_len;
1153        const int n_with_left = (int)chunk_len + pad_left;
1154        // PyTorch: unfold(size=frame_length+1, step=hop) on semicausal-padded waveform
1155        const int64_t pt_frames = (n_with_left - (hparams.audio_window_len + 1)) / hop + 1;
1156        const int64_t n_padded_needed = (pt_frames - 1) * hop + fft_size;
1157        const int total_pad = std::max((int)(n_padded_needed - (int)chunk_len), pad_left);
1158        std::vector<float> padded_samples(total_pad + chunk_len, 0.0f);
1159        std::copy(chunk_ptr, chunk_ptr + chunk_len, padded_samples.data() + pad_left);
1160
1161        mtmd_audio_mel out_chunk;
1162        bool ok = log_mel_spectrogram(padded_samples.data(), padded_samples.size(), 4, params, cache, out_chunk);
1163        if (!ok) {
1164            return false;
1165        }
1166
1167        // Trim to PyTorch frame count
1168        out_chunk.n_len = std::min(out_chunk.n_len, pt_frames);
1169
1170        output.push_back(std::move(out_chunk));
1171    }
1172
1173    return true;
1174}
1175
1176//
1177// mtmd_audio_preprocessor_parakeet implementation
1178//
1179
1180void mtmd_audio_preprocessor_parakeet::worker_thread(
1181                             int   ith,
1182                     const float * window_func,
1183                             int   window_size,
1184        const std::vector<float> & samples,
1185                             int   n_samples,
1186                             int   frame_size,
1187                             int   frame_step,
1188                             int   n_threads,
1189                             int   n_fft_bins,
1190          const mtmd_audio_cache & cache,
1191                  mtmd_audio_mel & mel) {
1192    std::vector<float> fft_in(frame_size * 2, 0.0);
1193    std::vector<float> fft_out(frame_size * 2 * 2 * 2);
1194
1195    int n_fb = n_fft_bins;
1196    int i = ith;
1197
1198    GGML_ASSERT(n_fb == 1 + (frame_size / 2));
1199
1200    const double eps = 5.960464477539063e-08;
1201
1202    for (; i < std::min(n_samples / frame_step + 1, (int) mel.n_len); i += n_threads) {
1203        const int offset = i * frame_step;
1204        const int window_pad_left = (frame_size - window_size) / 2;
1205
1206        // Zero-pad left.
1207        std::fill(fft_in.begin(), fft_in.begin() + window_pad_left, 0.0f);
1208
1209        // Apply windowed samples in the center.
1210        const int n_to_process = std::min({window_size, n_samples - offset});
1211        for (int j = 0; j < n_to_process; j++) {
1212            fft_in[window_pad_left + j] = window_func[j] * samples[offset + window_pad_left + j];
1213        }
1214
1215        // Zero-pad right.
1216        std::fill(fft_in.begin() + window_pad_left + n_to_process, fft_in.begin() + frame_size, 0.0f);
1217
1218        // FFT.
1219        fft(cache, fft_in.data(), frame_size, fft_out.data());
1220
1221        // Calculate modulus^2 of complex numbers.
1222        for (int j = 0; j < n_fb; j++) {
1223            fft_out[j] = (fft_out[2 * j + 0] * fft_out[2 * j + 0] + fft_out[2 * j + 1] * fft_out[2 * j + 1]);
1224        }
1225
1226        // mel spectrogram.
1227        for (int j = 0; j < mel.n_mel; j++) {
1228            double sum = 0.0;
1229            int k = 0;
1230            for (k = 0; k < n_fb - 3; k += 4) {
1231                sum +=
1232                    fft_out[k + 0] * cache.filters.data[j * n_fb + k + 0] +
1233                    fft_out[k + 1] * cache.filters.data[j * n_fb + k + 1] +
1234                    fft_out[k + 2] * cache.filters.data[j * n_fb + k + 2] +
1235                    fft_out[k + 3] * cache.filters.data[j * n_fb + k + 3];
1236            }
1237            for (; k < n_fb; k++) {
1238                sum += fft_out[k] * cache.filters.data[j * n_fb + k];
1239            }
1240            mel.data[j * mel.n_len + i] = std::log(sum + eps);
1241        }
1242    }
1243
1244    // Otherwise fft_out are all zero.
1245    const double empty_sum = std::log(eps);
1246    for (; i < mel.n_len; i += n_threads) {
1247        for (int j = 0; j < mel.n_mel; j++) {
1248            mel.data[j * mel.n_len + i] = empty_sum;
1249        }
1250    }
1251}
1252
1253void mtmd_audio_preprocessor_parakeet::initialize() {
1254    cache.fill_sin_cos_table(hparams.audio_n_fft);
1255
1256    const size_t n_fft = hparams.audio_n_fft / 2 + 1;
1257    GGML_ASSERT(hparams.mel_filters.size() == (size_t)hparams.n_mel_bins * n_fft);
1258    cache.filters.n_mel = hparams.n_mel_bins;
1259    cache.filters.n_fft = n_fft;
1260    cache.filters.data  = hparams.mel_filters;
1261
1262    GGML_ASSERT(hparams.window.size() == (size_t)hparams.audio_window_len);
1263    GGML_ASSERT(hparams.window.size() <= (size_t) hparams.audio_n_fft);
1264    cache.hann_window = hparams.window;
1265}
1266
1267bool mtmd_audio_preprocessor_parakeet::preprocess(const float * samples,
1268                                                       size_t   n_samples_in,
1269                                  std::vector<mtmd_audio_mel> & output) const {
1270    if (n_samples_in == 0) {
1271        return false;
1272    }
1273
1274    filter_params params;
1275    params.n_mel            = hparams.n_mel_bins;
1276    params.n_fft_bins       = 1 + (hparams.audio_n_fft / 2);
1277    params.hann_window_size = hparams.audio_window_len;
1278    params.hop_length       = hparams.audio_hop_len;
1279    params.sample_rate      = hparams.audio_sample_rate;
1280
1281    GGML_ASSERT(!cache.sin_vals.empty());
1282    GGML_ASSERT(!cache.cos_vals.empty());
1283    GGML_ASSERT(!cache.filters.data.empty());
1284
1285    const float * window_func = cache.hann_window.data();
1286    const int     window_size = params.hann_window_size;
1287    const int     frame_size  = (params.n_fft_bins - 1) * 2;
1288    const int     frame_step  = params.hop_length;
1289
1290    // Apply preemphasis filter (high-pass): x[i] = x[i] - 0.97 * x[i-1]
1291    std::vector<float> samples_preprocessed(samples, samples + n_samples_in);
1292    {
1293        const float preemph = 0.97f;
1294        for (int i = n_samples_in - 1; i > 0; i--) {
1295            samples_preprocessed[i] = samples_preprocessed[i] - preemph * samples_preprocessed[i - 1];
1296        }
1297    }
1298
1299    // Parakeet uses centered constant padding
1300    const size_t pad = (size_t)(frame_size / 2);
1301    std::vector<float> samples_padded(n_samples_in + 2 * pad, 0.0f);
1302    std::copy(samples_preprocessed.begin(), samples_preprocessed.end(), samples_padded.begin() + pad);
1303
1304    mtmd_audio_mel out_full;
1305    out_full.n_mel = params.n_mel;
1306    out_full.n_len = (samples_padded.size() - frame_size) / frame_step + 1;
1307    out_full.n_len_org = out_full.n_len;
1308    out_full.data.resize(out_full.n_mel * out_full.n_len);
1309
1310    const int n_threads = 4;
1311    std::vector<std::thread> workers(n_threads - 1);
1312    for (int iw = 0; iw < n_threads - 1; ++iw) {
1313        workers[iw] = std::thread(
1314            worker_thread, iw + 1,
1315            window_func,
1316            window_size,
1317            std::cref(samples_padded),
1318            samples_padded.size(),
1319            frame_size,
1320            frame_step,
1321            n_threads,
1322            params.n_fft_bins,
1323            std::cref(cache),
1324            std::ref(out_full)
1325        );
1326    }
1327
1328    worker_thread(0,
1329            window_func,
1330            window_size,
1331            samples_padded,
1332            samples_padded.size(),
1333            frame_size,
1334            frame_step,
1335            n_threads,
1336            params.n_fft_bins,
1337            cache,
1338            out_full);
1339
1340    for (int iw = 0; iw < n_threads - 1; ++iw) {
1341        workers[iw].join();
1342    }
1343
1344    // Per-feature normalization (only on valid frames)
1345    {
1346        const double eps = 1e-5;
1347        int valid_frames = n_samples_in / frame_step;
1348
1349        for (int j = 0; j < out_full.n_mel; j++) {
1350            double sum = 0.0;
1351            double sq_diff_sum = 0.0;
1352
1353            // Calculate Mean ONLY on valid audio frames
1354            for (int i = 0; i < valid_frames; i++) {
1355                sum += (double)out_full.data[j * out_full.n_len + i];
1356            }
1357            double mean = sum / valid_frames;
1358
1359            // Calculate Variance ONLY on valid audio frames
1360            for (int i = 0; i < valid_frames; i++) {
1361                double diff = (double)out_full.data[j * out_full.n_len + i] - mean;
1362                sq_diff_sum += diff * diff;
1363            }
1364
1365            double std_dev = std::sqrt(sq_diff_sum / (valid_frames - 1.0));
1366            double denominator = std_dev + eps;
1367
1368            // Apply to ALL frames (including the padded ones)
1369            for (int i = 0; i < out_full.n_len; i++) {
1370                out_full.data[j * out_full.n_len + i] = (float)((out_full.data[j * out_full.n_len + i] - mean) / denominator);
1371            }
1372        }
1373    }
1374
1375    output.push_back(std::move(out_full));
1376    return true;
1377}
1378
1379
1380// mtmd_audio_preprocessor_gemma4ua
1381//
1382
1383void mtmd_audio_preprocessor_gemma4ua::initialize() {
1384    // no-op: no FFT or filterbank needed
1385}
1386
1387bool mtmd_audio_preprocessor_gemma4ua::preprocess(const float *                 samples,
1388                                                   size_t                        n_samples,
1389                                                   std::vector<mtmd_audio_mel> & output) const {
1390    if (n_samples == 0) {
1391        return false;
1392    }
1393
1394    const int frame_size = hparams.n_mel_bins; // 640 samples per token @ 16 kHz = 40 ms
1395    const int n_tokens   = ((int)n_samples + frame_size - 1) / frame_size;
1396
1397    mtmd_audio_mel mel;
1398    mel.n_len     = n_tokens;
1399    mel.n_len_org = n_tokens;
1400    mel.n_mel     = frame_size;
1401    mel.data.assign((size_t)frame_size * n_tokens, 0.0f);
1402
1403    // Store mel-major (data[f * n_tokens + t]) so the ggml tensor loads as
1404    // [n_tokens, frame_size] with ne[0]=n_tokens, ne[1]=frame_size.
1405    // The graph builder transposes before RMSNorm so normalization is over frame_size.
1406    for (int t = 0; t < n_tokens; t++) {
1407        for (int f = 0; f < frame_size; f++) {
1408            size_t src = (size_t)t * frame_size + f;
1409            mel.data[(size_t)f * n_tokens + t] = (src < n_samples) ? samples[src] : 0.0f;
1410        }
1411    }
1412
1413    output.push_back(std::move(mel));
1414    return true;
1415}
1416
1417//
1418// mtmd_audio_streaming_istft implementation
1419//
1420
1421mtmd_audio_streaming_istft::mtmd_audio_streaming_istft(int n_fft, int hop_length) :
1422    n_fft(n_fft),
1423    hop_length(hop_length),
1424    n_fft_bins(n_fft / 2 + 1),
1425    overlap_buffer(n_fft, 0.0f),
1426    window_sum_buffer(n_fft, 0.0f),
1427    padding_to_remove((n_fft - hop_length) / 2),
1428    ifft_in(n_fft * 2 * 4, 0.0f),  // extra space for recursive IFFT
1429    ifft_out(n_fft * 2 * 4, 0.0f) {
1430    GGML_ASSERT(n_fft > 0 && hop_length > 0 && hop_length <= n_fft);
1431    cache.fill_sin_cos_table(n_fft);
1432    cache.fill_hann_window(n_fft, true);
1433}
1434
1435void mtmd_audio_streaming_istft::reset() {
1436    std::fill(overlap_buffer.begin(), overlap_buffer.end(), 0.0f);
1437    std::fill(window_sum_buffer.begin(), window_sum_buffer.end(), 0.0f);
1438    padding_to_remove = (n_fft - hop_length) / 2;
1439}
1440
1441std::vector<float> mtmd_audio_streaming_istft::process_frame(const float * frame_spectrum) {
1442    std::vector<float> output(hop_length);
1443
1444    // copy frequencies
1445    for (int j = 0; j < n_fft_bins; j++) {
1446        ifft_in[j * 2 + 0] = frame_spectrum[j * 2 + 0];
1447        ifft_in[j * 2 + 1] = frame_spectrum[j * 2 + 1];
1448    }
1449
1450    // mirror negative frequencies
1451    for (int j = 1; j < n_fft_bins - 1; j++) {
1452        int mirror_idx              = n_fft - j;
1453        ifft_in[mirror_idx * 2 + 0] = ifft_in[j * 2 + 0];
1454        ifft_in[mirror_idx * 2 + 1] = -ifft_in[j * 2 + 1];  // conjugate
1455    }
1456
1457    ifft(cache, ifft_in.data(), n_fft, ifft_out.data());
1458
1459    // update window sum and overlap buffer
1460    for (int j = 0; j < n_fft; j++) {
1461        window_sum_buffer[j] += cache.hann_window[j] * cache.hann_window[j];
1462        overlap_buffer[j] += ifft_out[j * 2] * cache.hann_window[j];
1463    }
1464
1465    // extract hop_length samples with normalization
1466    for (int i = 0; i < hop_length; i++) {
1467        if (window_sum_buffer[i] > 1e-8f) {
1468            output[i] = overlap_buffer[i] / window_sum_buffer[i];
1469        } else {
1470            output[i] = overlap_buffer[i];
1471        }
1472    }
1473
1474    // shift buffers left by hop_length
1475    std::copy(overlap_buffer.begin() + hop_length, overlap_buffer.end(), overlap_buffer.begin());
1476    std::fill(overlap_buffer.end() - hop_length, overlap_buffer.end(), 0.0f);
1477
1478    std::copy(window_sum_buffer.begin() + hop_length, window_sum_buffer.end(), window_sum_buffer.begin());
1479    std::fill(window_sum_buffer.end() - hop_length, window_sum_buffer.end(), 0.0f);
1480
1481    // Remove padding if needed
1482    int to_remove = std::min(padding_to_remove, (int) output.size());
1483    padding_to_remove -= to_remove;
1484    output.erase(output.begin(), output.begin() + to_remove);
1485
1486    return output;
1487}
1488
1489std::vector<float> mtmd_audio_streaming_istft::flush() {
1490    std::vector<float> output;
1491
1492    // Extract remaining samples from overlap buffer
1493    // Continue until we've extracted all meaningful samples
1494    int remaining = n_fft - hop_length;
1495    while (remaining > 0) {
1496        int chunk_size = std::min(remaining, hop_length);
1497
1498        for (int i = 0; i < chunk_size; i++) {
1499            float sample;
1500            if (window_sum_buffer[i] > 1e-8f) {
1501                sample = overlap_buffer[i] / window_sum_buffer[i];
1502            } else {
1503                sample = overlap_buffer[i];
1504            }
1505            output.push_back(sample);
1506        }
1507
1508        // Shift buffers
1509        std::copy(overlap_buffer.begin() + chunk_size, overlap_buffer.end(), overlap_buffer.begin());
1510        std::fill(overlap_buffer.end() - chunk_size, overlap_buffer.end(), 0.0f);
1511
1512        std::copy(window_sum_buffer.begin() + chunk_size, window_sum_buffer.end(), window_sum_buffer.begin());
1513        std::fill(window_sum_buffer.end() - chunk_size, window_sum_buffer.end(), 0.0f);
1514
1515        remaining -= chunk_size;
1516    }
1517
1518    return output;
1519}
1520
1521//
1522// mtmd_audio_preprocessor_pockettts
1523//
1524// mimi takes the raw 24kHz waveform, there is no mel front-end
1525// the samples are handed over as a single-row "mel", to reuse the normal chunk path
1526//
1527
1528bool mtmd_audio_preprocessor_pockettts::preprocess(const float *                 samples,
1529                                                   size_t                        n_samples,
1530                                                   std::vector<mtmd_audio_mel> & output) const {
1531    // the encoder needs whole frames, see pad_for_conv1d() in the reference
1532    const int64_t frame_size = (int64_t) hparams.mimi_downsample * 120;
1533    if (n_samples == 0 || frame_size <= 0) {
1534        return false;
1535    }
1536
1537    // the mimi transformer mask is dense, so cost is quadratic in the reference length
1538    const int64_t max_samples = (int64_t) clip_hparams::pockettts_max_spk_seconds * hparams.audio_sample_rate;
1539    if ((int64_t) n_samples > max_samples) {
1540        LOG_WRN("%s: speaker reference is %.1f s, truncating to the first %d s\n", __func__,
1541                (double) n_samples / hparams.audio_sample_rate, clip_hparams::pockettts_max_spk_seconds);
1542        n_samples = (size_t) max_samples;
1543    }
1544
1545    const int64_t n_frames  = (int64_t) (n_samples + frame_size - 1) / frame_size;
1546    const int64_t n_padded  = n_frames * frame_size;
1547
1548    mtmd_audio_mel out;
1549    out.n_mel     = 1;
1550    out.n_len     = n_padded;
1551    out.n_len_org = (int64_t) n_samples;
1552    out.data.assign((size_t) n_padded, 0.0f);
1553    std::copy(samples, samples + n_samples, out.data.begin());
1554
1555    output.push_back(std::move(out));
1556    return true;
1557}