char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
de8656bd9
char/aliceai-t5-moe
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 ) { 18sin_vals .resize (n ); 19cos_vals .resize (n ); 20for (uint32_t i = 0 ;i < n ;i ++ ) { 21double theta = (2 * M_PI * i ) /n ; 22sin_vals [i ]= sinf (theta ); 23cos_vals [i ]= cosf (theta ); 24 } 25} 26 27void mtmd_audio_cache::fill_hann_window (uint32_t length ,bool periodic ) { 28hann_window .resize (length ); 29int offset = periodic ?0 :-1 ; 30for (uint32_t i = 0 ;i < length ;i ++ ) { 31hann_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 , 36int64_t n_fft , 37int sample_rate , 38float fmin , 39float fmax , 40bool slaney_area_norm , 41float scale , 42bool use_htk ) { 43GGML_ASSERT (n_mel > 0 && n_fft > 1 ); 44if (fmax <=0.0f ) { 45fmax = 0.5f * sample_rate ; 46 } 47 48 std::function < double (double )> hz_to_mel ; 49 std::function < double (double )> mel_to_hz ; 50 51if (use_htk ) { 52hz_to_mel = [](const double f_hz )-> double { 53return 2595.0 * log10 (1.0 + f_hz /700.0 ); 54 }; 55mel_to_hz = [](const double m )-> double { 56return 700.0 * (pow (10.0 ,m /2595.0 )- 1.0 ); 57 }; 58 }else { 59// Slaney scale (matches librosa default) 60const double min_log_hz = 1000.0 ; 61const double lin_slope = 3 /200. ; 62const double min_log_mel = min_log_hz * lin_slope ; 63const double log_step = log (6.4 ) /27.0 ; 64hz_to_mel = [min_log_hz ,lin_slope ,log_step ,min_log_mel ](const double f_hz )-> double { 65return (f_hz < min_log_hz ) ?f_hz * lin_slope :min_log_mel + log (f_hz /min_log_hz ) /log_step ; 66 }; 67mel_to_hz = [min_log_hz ,lin_slope ,log_step ,min_log_mel ](const double m )-> double { 68return (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 73const double bin_hz_step = double (sample_rate ) /double (n_fft ); 74 75// mel grid: n_mel + 2 edges 76const double m_lo = hz_to_mel (fmin ); 77const double m_hi = hz_to_mel (fmax ); 78 std::vector < double > mel_pts (n_mel + 2 ); 79for (int i = 0 ;i < n_mel + 2 ;++ i ) { 80mel_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 ); 85for (int i = 0 ;i < n_mel + 2 ;++ i ) { 86hz_pts [i ]= mel_to_hz (mel_pts [i ]); 87 } 88 89const int64_t n_fft_bins = n_fft /2 + 1 ; 90 91// Validate allocation size 92if ((size_t )n_mel * (size_t )n_fft_bins > SIZE_MAX ) { 93GGML_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 ); 98for (int64_t m = 0 ;m < n_mel ;++ m ) { 99const double f_left = hz_pts [m ]; 100const double f_center = hz_pts [m + 1 ]; 101const double f_right = hz_pts [m + 2 ]; 102 103const double denom_l = std::max (1e-30 ,f_center - f_left ); 104const double denom_r = std::max (1e-30 ,f_right - f_center ); 105const double enorm = slaney_area_norm ? (2.0 / std::max (1e-30 ,f_right - f_left )) :1.0 ; 106 107for (int k = 0 ;k < n_fft_bins ;++ k ) { 108const double f = k * bin_hz_step ; 109double w = 0.0 ; 110if (f >=f_left && f <=f_center ) { 111w = (f - f_left ) /denom_l ; 112 }else if (f > f_center && f <=f_right ) { 113w = (f_right - f ) /denom_r ; 114 } 115out [size_t (m )* size_t (n_fft_bins )+ size_t (k )]= float (w * enorm * scale ); 116 } 117 } 118 119filters .n_mel = n_mel ; 120filters .n_fft = n_fft ; 121filters .data = std::move (out ); 122 123if (DEBUG ) {// debug 124for (size_t i = 0 ;i < filters .data .size ();++ i ) { 125if (filters .data [i ]!= 0.0f ) { 126printf ("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 ) { 140const int n_sin_cos_vals = cache .sin_vals .size (); 141const int sin_cos_step = n_sin_cos_vals /N ; 142 143constexpr float sign = Inverse ?1.0f :-1.0f ; 144const float scale = Inverse ? (1.0f /N ) :1.0f ; 145 146for (int k = 0 ;k < N ;k ++ ) { 147float re = 0 ; 148float im = 0 ; 149 150for (int n = 0 ;n < N ;n ++ ) { 151int idx = (k * n * sin_cos_step ) %n_sin_cos_vals ; 152float cos_val = cache .cos_vals [idx ]; 153float sin_val = cache .sin_vals [idx ]; 154 155if constexpr (RealInput ) { 156// Real input: in_im = 0, simplifies to: 157// re += in_re * cos_val 158// im += sign * in_re * sin_val 159float in_re = in [n ]; 160re += in_re * cos_val ; 161im += sign * in_re * sin_val ; 162 }else { 163float in_re = in [n * 2 + 0 ]; 164float in_im = in [n * 2 + 1 ]; 165// (a + bi) * (cos + sign*i*sin) = (a*cos - sign*b*sin) + (sign*a*sin + b*cos)i 166re += in_re * cos_val - sign * in_im * sin_val ; 167im += sign * in_re * sin_val + in_im * cos_val ; 168 } 169 } 170 171out [k * 2 + 0 ]= re * scale ; 172out [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 ) { 184GGML_ASSERT (N > 0 ); 185const int n_sin_cos_vals = cache .sin_vals .size (); 186 187if (N == 1 ) { 188out [0 ]= in [0 ]; 189if constexpr (RealInput ) { 190out [1 ]= 0.0f ; 191 }else { 192out [1 ]= in [1 ]; 193 } 194return ; 195 } 196 197const int half_N = N /2 ; 198if (N - half_N * 2 == 1 ) { 199// Odd N: fall back to DFT 200dft_impl < Inverse ,RealInput > (cache ,in ,N ,out ); 201return ; 202 } 203 204// Split into even and odd 205if constexpr (RealInput ) { 206// Real input: stride is 1, copy only real values 207float * even = in + N ; 208for (int i = 0 ;i < half_N ;++ i ) { 209even [i ]= in [2 * i ]; 210 } 211float * even_fft = out + 2 * N ; 212fft_impl < Inverse , true> (cache ,even ,half_N ,even_fft ); 213 214float * odd = even ; 215for (int i = 0 ;i < half_N ;++ i ) { 216odd [i ]= in [2 * i + 1 ]; 217 } 218float * odd_fft = even_fft + N ; 219fft_impl < Inverse , true> (cache ,odd ,half_N ,odd_fft ); 220 }else { 221// Complex input: stride is 2, copy complex pairs 222float * even = in + N * 2 ; 223for (int i = 0 ;i < half_N ;++ i ) { 224even [i * 2 + 0 ]= in [2 * i * 2 + 0 ]; 225even [i * 2 + 1 ]= in [2 * i * 2 + 1 ]; 226 } 227float * even_fft = out + 2 * N ; 228fft_impl < Inverse , false> (cache ,even ,half_N ,even_fft ); 229 230float * odd = even ; 231for (int i = 0 ;i < half_N ;++ i ) { 232odd [i * 2 + 0 ]= in [(2 * i + 1 )* 2 + 0 ]; 233odd [i * 2 + 1 ]= in [(2 * i + 1 )* 2 + 1 ]; 234 } 235float * odd_fft = even_fft + N ; 236fft_impl < Inverse , false> (cache ,odd ,half_N ,odd_fft ); 237 } 238 239float * even_fft = out + 2 * N ; 240float * odd_fft = even_fft + N ; 241 242const int sin_cos_step = n_sin_cos_vals /N ; 243 244constexpr float sign = Inverse ?1.0f :-1.0f ; 245constexpr float scale = Inverse ?0.5f :1.0f ; 246 247for (int k = 0 ;k < half_N ;k ++ ) { 248int idx = k * sin_cos_step ;// t = 2*M_PI*k/N 249float re = cache .cos_vals [idx ]; 250float im = sign * cache .sin_vals [idx ]; 251 252float re_odd = odd_fft [2 * k + 0 ]; 253float im_odd = odd_fft [2 * k + 1 ]; 254 255out [2 * k + 0 ]= scale * (even_fft [2 * k + 0 ]+ re * re_odd - im * im_odd ); 256out [2 * k + 1 ]= scale * (even_fft [2 * k + 1 ]+ re * im_odd + im * re_odd ); 257 258out [2 * (k + half_N )+ 0 ]= scale * (even_fft [2 * k + 0 ]- re * re_odd + im * im_odd ); 259out [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 ) { 265fft_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 ) { 270fft_impl < true, false> (cache ,in ,N ,out ); 271} 272 273struct filter_params { 274int64_t n_mel ; 275int64_t n_fft_bins ; 276int32_t hann_window_size ; 277int32_t hop_length ; 278int32_t sample_rate ; 279bool no_padding = false; 280bool center_padding = false; 281float preemph = 0.f ; 282bool use_natural_log = false; 283bool norm_per_feature = false; 284bool use_magnitude = false;// |X| instead of |X|^2 285float mel_floor = 5.960464477539063e-08f ; 286}; 287 288static void log_mel_spectrogram_worker_thread (int ith , 289const float * hann , 290const std::vector < float > & samples , 291int n_samples , 292int frame_size , 293int frame_step , 294int n_threads , 295const filter_params & params , 296const mtmd_audio_cache & cache , 297mtmd_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 301int64_t n_fft_bins = params .n_fft_bins ; 302int64_t i = ith ; 303 304const auto & filters = cache .filters ; 305 306// make sure n_fft == 1 + (WHISPER_N_FFT / 2), bin_0 to bin_nyquist 307GGML_ASSERT (n_fft_bins == 1 + (frame_size /2 )); 308GGML_ASSERT (cache .sin_vals .size ()== cache .cos_vals .size ()); 309// calculate FFT only when fft_in are not all zero 310for (;i < std::min ((int64_t )(n_samples /frame_step + 1 ),out .n_len );i += n_threads ) { 311const int64_t offset = i * frame_step ; 312 313// apply Hann window (~10% faster) 314const int valid_len = std::min (frame_size , std::max (0 ,n_samples - (int )offset )); 315for (int j = 0 ;j < valid_len ;j ++ ) { 316fft_in [j ]= hann [j ]* samples [offset + j ]; 317 } 318 319// fill the rest with zeros 320if (valid_len < frame_size ) { 321 std::fill (fft_in .begin ()+ valid_len ,fft_in .end (),0.0 ); 322 } 323 324// FFT 325fft (cache ,fft_in .data (),frame_size ,fft_out .data ()); 326 327// Calculate modulus^2 (power) or modulus (magnitude) 328for (int j = 0 ;j < n_fft_bins ;j ++ ) { 329float power = (fft_out [2 * j + 0 ]* fft_out [2 * j + 0 ]+ fft_out [2 * j + 1 ]* fft_out [2 * j + 1 ]); 330fft_out [j ]= params .use_magnitude ?sqrtf (power ) :power ; 331 } 332 333// mel spectrogram 334for (int64_t j = 0 ;j < out .n_mel ;j ++ ) { 335double sum = 0.0 ; 336// unroll loop (suggested by GH user @lunixbochs) 337int k = 0 ; 338for (k = 0 ;k < n_fft_bins - 3 ;k += 4 ) { 339size_t idx = size_t (j )* size_t (n_fft_bins )+ size_t (k ); 340sum += 341fft_out [k + 0 ]* filters .data [idx + 0 ]+ 342fft_out [k + 1 ]* filters .data [idx + 1 ]+ 343fft_out [k + 2 ]* filters .data [idx + 2 ]+ 344fft_out [k + 3 ]* filters .data [idx + 3 ]; 345 } 346// handle n_fft remainder 347for (;k < n_fft_bins ;k ++ ) { 348sum += fft_out [k ]* filters .data [(size_t )j * n_fft_bins + k ]; 349 } 350sum = std::max (sum , (double )params .mel_floor ); 351sum = params .use_natural_log 352 ?log (sum ) 353 :log10 (sum ); 354out .data [(size_t )j * out .n_len + i ]= sum ; 355 } 356 } 357 358// Otherwise fft_out are all zero 359double sum = params .use_natural_log ?log (1e-10 ) :log10 (1e-10 ); 360for (;i < out .n_len ;i += n_threads ) { 361for (int64_t j = 0 ;j < out .n_mel ;j ++ ) { 362out .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 ( 369const float * samples , 370const int n_samples_in , 371const int n_threads , 372const filter_params & params , 373const mtmd_audio_cache & cache , 374mtmd_audio_mel & out ) { 375//const int64_t t_start_us = ggml_time_us(); 376 377out .n_len_org = n_samples_in ; 378int n_samples = n_samples_in ; 379 380// Hann window 381const float * hann = cache .hann_window .data (); 382const int frame_size = (params .n_fft_bins - 1 )* 2 ; 383const int frame_step = params .hop_length ; 384 385// Padding 386 std::vector < float > samples_padded ; 387if (params .no_padding ) { 388// no padding, use samples as-is 389samples_padded = std::vector < float > (samples ,samples + n_samples ); 390samples = samples_padded .data (); 391n_samples = samples_padded .size (); 392 }else if (params .center_padding ) { 393const auto pad_amount = frame_size /2 ; 394samples_padded = std::vector < float > (n_samples + 2 * pad_amount ,0 ); 395 std::copy (samples ,samples + n_samples ,samples_padded .data ()+ pad_amount ); 396samples = samples_padded .data (); 397n_samples = samples_padded .size (); 398 }else { 399// existing padding logic 400int64_t stage_1_pad = params .sample_rate * 30 ; 401int64_t stage_2_pad = frame_size /2 ; 402samples_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 407if (n_samples < stage_2_pad + 1 ) { 408// TODO: Handle short audio differently or return error 409return 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 415samples = samples_padded .data (); 416n_samples = samples_padded .size (); 417 } 418 419// preemphasis 420if (params .preemph ) { 421const int pad_amount = frame_size /2 ; 422const float preemph = 0.97f ; 423float prev = samples_padded [pad_amount ]; 424for (int i = pad_amount + 1 ;i + pad_amount < n_samples ;++ i ) { 425float cur = samples_padded [i ]; 426samples_padded [i ]= cur - preemph * prev ; 427prev = 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 ; 434if (params .hann_window_size < frame_size ) { 435hann_window_padded .resize (frame_size ); 436const int padding = (frame_size - params .hann_window_size ) /2 ; 437 std::copy (hann ,hann + params .hann_window_size ,& hann_window_padded [padding ]); 438hann = hann_window_padded .data (); 439 } 440 441 442GGML_ASSERT (params .n_fft_bins > 0 ); 443GGML_ASSERT (params .hop_length > 0 ); 444out .n_mel = params .n_mel ; 445out .n_len = (n_samples - frame_size ) /frame_step + 1 ; 446// Validate dimensions before allocation to prevent integer overflow 447if (out .n_mel <=0 || out .n_len <=0 ) { 448LOG_ERR ("%s: invalid mel dimensions n_mel=%lld n_len=%lld\n" ,__func__ , (long long )out .n_mel , (long long )out .n_len ); 449return false; 450 } 451const size_t total_size = (size_t )out .n_mel * (size_t )out .n_len ; 452if (total_size > SIZE_MAX /sizeof (float )) { 453LOG_ERR ("%s: size overflow: n_mel=%lld n_len=%lld\n" ,__func__ , (long long )out .n_mel , (long long )out .n_len ); 454return false; 455 } 456if (n_samples < frame_size ) { 457LOG_ERR ("%s: not enough samples after padding\n" ,__func__ ); 458return false; 459 } 460out .data .resize (total_size ); 461 462 { 463 std::vector < std::thread > workers (n_threads - 1 ); 464for (int iw = 0 ;iw < n_threads - 1 ;++ iw ) { 465workers [iw ]= 466 std::thread (log_mel_spectrogram_worker_thread ,iw + 1 ,hann , std::cref (samples_padded ),n_samples , 467frame_size ,frame_step ,n_threads , std::cref (params ), std::cref (cache ), std::ref (out )); 468 } 469 470// main thread 471log_mel_spectrogram_worker_thread (0 ,hann ,samples_padded ,n_samples ,frame_size ,frame_step ,n_threads ,params , 472cache ,out ); 473for (int iw = 0 ;iw < n_threads - 1 ;++ iw ) { 474workers [iw ].join (); 475 } 476 } 477 478const int64_t effective_n_len = n_samples_in /frame_step ; 479if (params .norm_per_feature ) { 480GGML_ASSERT (effective_n_len > 1 ); 481for (int64_t i = 0 ;i < out .n_mel ;i ++ ) { 482double mean = 0 ; 483for (int64_t j = 0 ;j < effective_n_len ;++ j ) { 484mean += out .data [(size_t )i * out .n_len + j ]; 485 } 486mean /=effective_n_len ; 487 488double var = 0.0 ; 489for (int64_t j = 0 ;j < effective_n_len ;++ j ) { 490const double value = out .data [(size_t )i * out .n_len + j ]- mean ; 491var += value * value ; 492 } 493var /=effective_n_len - 1 ;// unbiased 494const double mstd = std::sqrt (var + 1e-5 ); 495 496for (int64_t j = 0 ;j < effective_n_len ;++ j ) { 497auto & value = out .data [(size_t )i * out .n_len + j ]; 498value = (value - mean ) /mstd ; 499 } 500 501// pad the rest with zeros 502for (int64_t j = effective_n_len ;j < out .n_len ;++ j ) { 503out .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) 508double mmax = -1e20 ; 509const size_t mel_size = (size_t )out .n_mel * (size_t )out .n_len ; 510for (size_t i = 0 ;i < mel_size ;i ++ ) { 511if (out .data [i ]> mmax ) { 512mmax = out .data [i ]; 513 } 514 } 515 516mmax -= 8.0 ; 517 518for (size_t i = 0 ;i < mel_size ;i ++ ) { 519if (out .data [i ]< mmax ) { 520out .data [i ]= mmax ; 521 } 522out .data [i ]= (out .data [i ]+ 4.0 )/4.0 ; 523 } 524 } 525 526// Dump log_mel_spectrogram 527if (DEBUG ) { 528 std::ofstream outFile ("log_mel_spectrogram.json" ); 529outFile <<"[" ; 530for (uint64_t i = 0 ;i < out .data .size ()- 1 ;i ++ ) { 531outFile <<out .data [i ] <<", " ; 532 } 533outFile <<out .data [out .data .size ()- 1 ] <<"]" ; 534outFile .close (); 535 } 536 537return true; 538} 539 540// 541// mtmd_audio_preprocessor_whisper 542// 543 544void mtmd_audio_preprocessor_whisper::initialize () { 545cache .fill_sin_cos_table (hparams .audio_n_fft ); 546cache .fill_hann_window (hparams .audio_window_len , true); 547cache .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 , 551size_t n_samples , 552 std::vector < mtmd_audio_mel > & output )const { 553if (n_samples == 0 ) { 554// empty audio 555return false; 556 } 557 558 std::vector < float > smpl ; 559// reflection padding needs one sample plus half an FFT window 560size_t min_samples = (size_t )hparams .audio_n_fft /2 + 1 ; 561if (n_samples < min_samples ) { 562smpl .resize (min_samples ,0.0f ); 563 std::memcpy (smpl .data (),samples ,n_samples * sizeof (float )); 564samples = smpl .data (); 565n_samples = smpl .size (); 566 } 567 568filter_params params ; 569params .n_mel = hparams .n_mel_bins ; 570params .n_fft_bins = 1 + (hparams .audio_n_fft /2 ); 571params .hann_window_size = hparams .audio_window_len ; 572params .hop_length = hparams .audio_hop_len ; 573params .sample_rate = hparams .audio_sample_rate ; 574params .center_padding = false; 575params .preemph = 0.0f ;// disabled 576params .use_natural_log = false; 577params .norm_per_feature = false; 578 579// make sure the cache is initialized 580GGML_ASSERT (!cache .sin_vals .empty ()); 581GGML_ASSERT (!cache .cos_vals .empty ()); 582GGML_ASSERT (!cache .filters .data .empty ()); 583 584mtmd_audio_mel out_full ; 585bool ok = log_mel_spectrogram (samples ,n_samples , 5864 ,// n_threads 587params ,cache ,out_full ); 588if (!ok ) { 589return 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 594if (DEBUG ) { 595printf ("output: n_mel = %d, n_len = %d\n" , (int )out_full .n_mel , (int )out_full .n_len ); 596 } 597const size_t frames_per_chunk = 3000 ; 598GGML_ASSERT ((size_t )out_full .n_len > frames_per_chunk ); 599for (size_t off = 0 ;off < (size_t )out_full .n_len ;off += frames_per_chunk ) { 600int64_t n_len = std::min ((int64_t )frames_per_chunk ,out_full .n_len - (int64_t )off ); 601if (n_len < (int64_t )frames_per_chunk ) { 602break ;// last incomplete chunk will always be a padded chunk, safe to ignore 603 } 604 605mtmd_audio_mel out_chunk ; 606out_chunk .n_len = n_len ; 607out_chunk .n_mel = out_full .n_mel ; 608out_chunk .n_len_org = out_full .n_mel ;// unused 609out_chunk .data .reserve ((size_t )out_chunk .n_mel * (size_t )out_chunk .n_len ); 610 611for (int64_t i = 0 ;i < out_full .n_mel ;i ++ ) { 612auto src = out_full .data .begin ()+ (size_t )i * out_full .n_len + off ; 613out_chunk .data .insert (out_chunk .data .end (),src ,src + frames_per_chunk ); 614 } 615 616output .push_back (std::move (out_chunk )); 617 } 618 619return 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 () { 633cache .fill_sin_cos_table (hparams .audio_n_fft ); 634cache .fill_hann_window (hparams .audio_window_len , true); 635cache .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 , 639size_t n_samples , 640 std::vector < mtmd_audio_mel > & output )const { 641if (n_samples == 0 ) { 642return false; 643 } 644 645GGML_ASSERT (!cache .sin_vals .empty ()); 646GGML_ASSERT (!cache .cos_vals .empty ()); 647GGML_ASSERT (!cache .filters .data .empty ()); 648 649// Reflection-pad n_fft/2 samples at each end, matching WhisperFeatureExtractor center=True 650const 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) 654for (int i = 0 ;i < pad ;i ++ ) { 655int src = pad - i ;// samples[pad], samples[pad-1], ..., samples[1] 656padded [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) 660for (int i = 0 ;i < pad ;i ++ ) { 661int src = (int )n_samples - 2 - i ;// samples[n-2], samples[n-3], ... 662padded [n_samples + pad + i ]= (src >=0 ) ?samples [src ] :0.0f ; 663 } 664 665filter_params params ; 666params .n_mel = hparams .n_mel_bins ; 667params .n_fft_bins = 1 + (hparams .audio_n_fft /2 ); 668params .hann_window_size = hparams .audio_window_len ; 669params .hop_length = hparams .audio_hop_len ; 670params .sample_rate = hparams .audio_sample_rate ; 671params .no_padding = true;// reflection padding already applied above 672params .use_natural_log = false;// log10 673 674mtmd_audio_mel mel_full ; 675bool ok = log_mel_spectrogram (padded .data (), (int )padded .size (),4 ,params ,cache ,mel_full ); 676if (!ok ) { 677return false; 678 } 679 680// Whisper-style normalization: clamp to (max - 8), scale to [-1, 1] 681 { 682double mmax = -1e20 ; 683for (float v :mel_full .data ) { 684if (v > mmax )mmax = v ; 685 } 686mmax -= 8.0 ; 687for (float & v :mel_full .data ) { 688v = (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. 694const 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. 700const int chunk_size = 100 ;// conv sub-chunk size (n_window * 2, n_window=50) 701const int window_size = 800 ;// mel frames per forward pass (n_window_infer=800) 702 703for (int64_t off = 0 ;off < n_eff ;off += window_size ) { 704const int64_t win_eff = std::min ((int64_t )window_size ,n_eff - off ); 705const int64_t n_chunks = (win_eff + chunk_size - 1 ) /chunk_size ; 706const int64_t n_padded = n_chunks * chunk_size ; 707 708mtmd_audio_mel out ; 709out .n_mel = mel_full .n_mel ; 710out .n_len = n_padded ; 711out .n_len_org = win_eff ; 712out .data .assign ((size_t )out .n_mel * (size_t )out .n_len ,0.0f ); 713for (int64_t m = 0 ;m < out .n_mel ;m ++ ) { 714const int64_t copy_len = std::min ((int64_t )win_eff ,mel_full .n_len - off ); 715if (copy_len > 0 ) { 716 std::copy (mel_full .data .begin ()+ (size_t )m * mel_full .n_len + off , 717mel_full .data .begin ()+ (size_t )m * mel_full .n_len + off + copy_len , 718out .data .begin ()+ (size_t )m * out .n_len ); 719 } 720 } 721output .push_back (std::move (out )); 722 } 723return 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 () { 735cache .fill_sin_cos_table (hparams .audio_n_fft ); 736cache .fill_hann_window (hparams .audio_window_len , true); 737cache .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 , 741size_t n_samples , 742 std::vector < mtmd_audio_mel > & output )const { 743if (n_samples == 0 ) { 744return false; 745 } 746 747GGML_ASSERT (!cache .sin_vals .empty ()); 748GGML_ASSERT (!cache .cos_vals .empty ()); 749GGML_ASSERT (!cache .filters .data .empty ()); 750 751const int pad = hparams .audio_n_fft /2 ;// center=True padding 752const int hop = hparams .audio_hop_len ; 753const size_t chunk_samples = (size_t )hparams .audio_chunk_len * hparams .audio_sample_rate ; 754 755for (size_t start = 0 ;start < n_samples ;start += chunk_samples ) { 756const size_t n_chunk = std::min (chunk_samples ,n_samples - start ); 757const float * chunk = samples + start ; 758 759const int64_t n_valid = n_chunk /hop ; 760if (n_valid == 0 ) { 761continue ;// 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 ); 767for (int i = 0 ;i < pad ;i ++ ) { 768int src = pad - i ; 769padded [i ]= (src < (int )n_chunk ) ?chunk [src ] :0.0f ; 770 } 771 std::copy (chunk ,chunk + n_chunk ,padded .begin ()+ pad ); 772if (n_chunk == chunk_samples ) { 773for (int i = 0 ;i < pad ;i ++ ) { 774int src = (int )n_chunk - 2 - i ; 775padded [n_chunk + pad + i ]= (src >=0 ) ?chunk [src ] :0.0f ; 776 } 777 } 778 779filter_params params ; 780params .n_mel = hparams .n_mel_bins ; 781params .n_fft_bins = 1 + (hparams .audio_n_fft /2 ); 782params .hann_window_size = hparams .audio_window_len ; 783params .hop_length = hop ; 784params .sample_rate = hparams .audio_sample_rate ; 785params .no_padding = true;// padding already applied above 786params .use_natural_log = false; 787 788mtmd_audio_mel mel_full ; 789if (!log_mel_spectrogram (padded .data (), (int )padded .size (),4 ,params ,cache ,mel_full )) { 790return false; 791 } 792GGML_ASSERT (mel_full .n_len >=n_valid ); 793 794// per-chunk whisper-style normalization, then keep only the valid frames 795mtmd_audio_mel out ; 796out .n_mel = mel_full .n_mel ; 797out .n_len = n_valid ; 798out .n_len_org = n_valid ; 799out .data .resize ((size_t )out .n_mel * (size_t )out .n_len ); 800 801double mmax = -1e20 ; 802for (int64_t m = 0 ;m < out .n_mel ;m ++ ) { 803for (int64_t t = 0 ;t < n_valid ;t ++ ) { 804mmax = std::max (mmax , (double )mel_full .data [(size_t )m * mel_full .n_len + t ]); 805 } 806 } 807mmax -= 8.0 ; 808for (int64_t m = 0 ;m < out .n_mel ;m ++ ) { 809for (int64_t t = 0 ;t < n_valid ;t ++ ) { 810const double v = std::max ((double )mel_full .data [(size_t )m * mel_full .n_len + t ],mmax ); 811out .data [(size_t )m * n_valid + t ]= (float ) ((v + 4.0 ) /4.0 ); 812 } 813 } 814 815output .push_back (std::move (out )); 816 } 817return !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 () { 829cache .fill_sin_cos_table (hparams .audio_n_fft ); 830cache .fill_hann_window (hparams .audio_window_len , true); 831cache .fill_mel_filterbank_matrix ( 832hparams .n_mel_bins ,hparams .audio_n_fft ,hparams .audio_sample_rate , 8330.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 , 841size_t n_samples , 842 std::vector < mtmd_audio_mel > & output )const { 843if (n_samples == 0 ) { 844return false; 845 } 846 847GGML_ASSERT (!cache .sin_vals .empty ()); 848GGML_ASSERT (!cache .cos_vals .empty ()); 849GGML_ASSERT (!cache .filters .data .empty ()); 850 851const int pad = hparams .audio_n_fft /2 ; 852 853 std::vector < float > padded (n_samples + 2 * pad ,0.0f ); 854for (int i = 0 ;i < pad ;i ++ ) { 855int src = pad - i ; 856padded [i ]= (src < (int )n_samples ) ?samples [src ] :0.0f ; 857 } 858 std::copy (samples ,samples + n_samples ,padded .begin ()+ pad ); 859for (int i = 0 ;i < pad ;i ++ ) { 860int src = (int )n_samples - 2 - i ; 861padded [n_samples + pad + i ]= (src >=0 ) ?samples [src ] :0.0f ; 862 } 863 864filter_params params ; 865params .n_mel = hparams .n_mel_bins ; 866params .n_fft_bins = 1 + (hparams .audio_n_fft /2 ); 867params .hann_window_size = hparams .audio_window_len ; 868params .hop_length = hparams .audio_hop_len ; 869params .sample_rate = hparams .audio_sample_rate ; 870params .no_padding = true;// reflect padding already applied above 871params .use_natural_log = true; 872params .use_magnitude = true; 873params .mel_floor = 1e-7f ; 874params .norm_per_feature = false; 875 876mtmd_audio_mel out ; 877bool ok = log_mel_spectrogram (padded .data (), (int )padded .size (),4 ,params ,cache ,out ); 878if (!ok ) { 879return false; 880 } 881 882output .push_back (std::move (out )); 883return 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 () { 894cache .fill_sin_cos_table (hparams .audio_n_fft ); 895cache .fill_hann_window (hparams .audio_window_len , true); 896cache .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 , 900size_t n_samples , 901 std::vector < mtmd_audio_mel > & output )const { 902if (n_samples == 0 ) { 903return false; 904 } 905 906GGML_ASSERT (!cache .sin_vals .empty ()); 907GGML_ASSERT (!cache .cos_vals .empty ()); 908GGML_ASSERT (!cache .filters .data .empty ()); 909 910// reflect pad by (n_fft - hop) / 2 = 384, matching center=False STFT framing 911const int pad = (hparams .audio_n_fft - hparams .audio_hop_len ) /2 ; 912if (n_samples < (size_t )pad + 1 ) { 913return false; 914 } 915 916 std::vector < float > padded (n_samples + 2 * pad ,0.0f ); 917for (int i = 0 ;i < pad ;i ++ ) { 918padded [i ]= samples [pad - i ]; 919 } 920 std::copy (samples ,samples + n_samples ,padded .begin ()+ pad ); 921for (int i = 0 ;i < pad ;i ++ ) { 922padded [n_samples + pad + i ]= samples [n_samples - 2 - i ]; 923 } 924 925filter_params params ; 926params .n_mel = hparams .n_mel_bins ; 927params .n_fft_bins = 1 + (hparams .audio_n_fft /2 ); 928params .hann_window_size = hparams .audio_window_len ; 929params .hop_length = hparams .audio_hop_len ; 930params .sample_rate = hparams .audio_sample_rate ; 931params .no_padding = true;// reflect padding already applied above 932params .use_natural_log = true; 933params .use_magnitude = true; 934params .mel_floor = 1e-5f ; 935 936mtmd_audio_mel out ; 937bool ok = log_mel_spectrogram (padded .data (), (int )padded .size (),4 ,params ,cache ,out ); 938if (!ok ) { 939return false; 940 } 941 942output .push_back (std::move (out )); 943return true; 944} 945 946// 947// mtmd_audio_preprocessor_conformer 948// 949 950void mtmd_audio_preprocessor_conformer::initialize () { 951cache .fill_sin_cos_table (hparams .audio_n_fft ); 952cache .fill_hann_window (hparams .audio_window_len , true); 953cache .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 , 957size_t n_samples , 958 std::vector < mtmd_audio_mel > & output )const { 959// empty audio 960if (n_samples == 0 ) { 961return false; 962 } 963 964filter_params params ; 965params .n_mel = hparams .n_mel_bins ; 966params .n_fft_bins = 1 + (hparams .audio_n_fft /2 ); 967params .hann_window_size = hparams .audio_window_len ; 968params .hop_length = hparams .audio_hop_len ; 969params .sample_rate = hparams .audio_sample_rate ; 970params .center_padding = true; 971params .preemph = 0.97f ; 972params .use_natural_log = true; 973params .norm_per_feature = true; 974 975// make sure the cache is initialized 976GGML_ASSERT (!cache .sin_vals .empty ()); 977GGML_ASSERT (!cache .cos_vals .empty ()); 978GGML_ASSERT (!cache .filters .data .empty ()); 979 980mtmd_audio_mel out_full ; 981bool ok = log_mel_spectrogram (samples ,n_samples , 9824 ,// n_threads 983params ,cache ,out_full ); 984if (!ok ) { 985return false; 986 } 987 988output .push_back (std::move (out_full )); 989return true; 990} 991 992// 993// mtmd_audio_preprocessor_granite_speech 994// 995 996void mtmd_audio_preprocessor_granite_speech::initialize () { 997cache .fill_sin_cos_table (hparams .audio_n_fft ); 998cache .fill_hann_window (hparams .audio_window_len , true); 999cache .fill_mel_filterbank_matrix ( 1000hparams .n_mel_bins /2 ,hparams .audio_n_fft ,hparams .audio_sample_rate , 10010.0f ,-1.0f , false,1.0f , true); 1002} 1003 1004bool mtmd_audio_preprocessor_granite_speech::preprocess (const float * samples , 1005size_t n_samples , 1006 std::vector < mtmd_audio_mel > & output )const { 1007if (n_samples == 0 ) { 1008return false; 1009 } 1010 1011GGML_ASSERT (!cache .sin_vals .empty ()); 1012GGML_ASSERT (!cache .cos_vals .empty ()); 1013GGML_ASSERT (!cache .filters .data .empty ()); 1014 1015const int n_fft = hparams .audio_n_fft ; 1016const int pad = n_fft /2 ; 1017 1018// reflect padding 1019const 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 ); 1022for (int i = 0 ;i < pad ;i ++ ) { 1023int src = i + 1 ; 1024if (src >= (int )n_samples ) { 1025src = (int )n_samples - 1 ; 1026 } 1027padded [pad - 1 - i ]= samples [src ]; 1028 } 1029for (int i = 0 ;i < pad ;i ++ ) { 1030int src = (int )n_samples - 2 - i ; 1031if (src < 0 ) { 1032src = 0 ; 1033 } 1034padded [pad + (int )n_samples + i ]= samples [src ]; 1035 } 1036 1037filter_params params ; 1038params .n_mel = hparams .n_mel_bins /2 ; 1039params .n_fft_bins = 1 + (n_fft /2 ); 1040params .hann_window_size = hparams .audio_window_len ; 1041params .hop_length = hparams .audio_hop_len ; 1042params .sample_rate = hparams .audio_sample_rate ; 1043params .no_padding = true; 1044params .center_padding = false; 1045params .preemph = 0.0f ; 1046params .use_natural_log = false; 1047params .norm_per_feature = false; 1048params .mel_floor = 1e-10f ; 1049 1050mtmd_audio_mel mel ; 1051if (!log_mel_spectrogram (padded .data (),n_padded ,4 ,params ,cache ,mel )) { 1052return false; 1053 } 1054 1055double mmax = -1e20 ; 1056const size_t mel_size = (size_t )mel .n_mel * (size_t )mel .n_len ; 1057for (size_t i = 0 ;i < mel_size ;i ++ ) { 1058if (mel .data [i ]> mmax ) { 1059mmax = mel .data [i ]; 1060 } 1061 } 1062mmax -= 8.0 ; 1063 1064for (size_t i = 0 ;i < mel_size ;i ++ ) { 1065if (mel .data [i ]< mmax ) { 1066mel .data [i ]= mmax ; 1067 } 1068mel .data [i ]= (mel .data [i ]+ 4.0 ) /4.0 ; 1069 } 1070 1071int64_t n_frames = mel .n_len ; 1072if (n_frames %2 == 1 ) { 1073n_frames -- ; 1074 } 1075const int64_t n_mel = mel .n_mel ; 1076const int64_t n_stacked = n_frames /2 ; 1077 1078mtmd_audio_mel stacked ; 1079stacked .n_mel = 2 * n_mel ; 1080stacked .n_len = n_stacked ; 1081stacked .n_len_org = (int64_t )n_samples ; 1082stacked .data .resize ((size_t )2 * (size_t )n_mel * (size_t )n_stacked ); 1083 1084for (int64_t t = 0 ;t < n_stacked ;t ++ ) { 1085for (int64_t m = 0 ;m < n_mel ;m ++ ) { 1086stacked .data [(size_t )m * n_stacked + t ]= mel .data [(size_t )m * mel .n_len + 2 * t ]; 1087stacked .data [(size_t )(m + n_mel )* n_stacked + t ]= mel .data [(size_t )m * mel .n_len + 2 * t + 1 ]; 1088 } 1089 } 1090 1091output .push_back (std::move (stacked )); 1092return true; 1093} 1094 1095// 1096// mtmd_audio_preprocessor_gemma4a 1097// 1098 1099void mtmd_audio_preprocessor_gemma4a::initialize () { 1100cache .fill_sin_cos_table (hparams .audio_n_fft ); 1101 1102// Standard periodic Hann window, zero-padded to FFT size 1103cache .hann_window .assign (hparams .audio_n_fft ,0.0f ); 1104for (uint32_t i = 0 ;i < (uint32_t )hparams .audio_window_len ;i ++ ) { 1105cache .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 1109cache .fill_mel_filterbank_matrix ( 1110hparams .n_mel_bins ,hparams .audio_n_fft ,hparams .audio_sample_rate , 11110.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 , 1119size_t n_samples , 1120 std::vector < mtmd_audio_mel > & output )const { 1121if (n_samples == 0 ) { 1122return false; 1123 } 1124 1125GGML_ASSERT (!cache .sin_vals .empty ()); 1126GGML_ASSERT (!cache .cos_vals .empty ()); 1127GGML_ASSERT (!cache .filters .data .empty ()); 1128 1129filter_params params ; 1130params .n_mel = hparams .n_mel_bins ; 1131params .n_fft_bins = 1 + (hparams .audio_n_fft /2 ); 1132params .hann_window_size = hparams .audio_n_fft ;// window is zero-padded to FFT size 1133params .hop_length = hparams .audio_hop_len ; 1134params .sample_rate = hparams .audio_sample_rate ; 1135params .no_padding = true; 1136params .center_padding = false; 1137params .preemph = 0.0f ; 1138params .use_natural_log = true; 1139params .use_magnitude = true; 1140params .mel_floor = 0.001f ; 1141params .norm_per_feature = false; 1142 1143// Split into 30-second chunks (model context limit, ~750 tokens each) 1144const size_t chunk_samples = 30 * hparams .audio_sample_rate ; 1145for (size_t off = 0 ;off < n_samples ;off += chunk_samples ) { 1146const float * chunk_ptr = samples + off ; 1147size_t chunk_len = std::min (chunk_samples ,n_samples - off ); 1148 1149// Semicausal left-padding + right-padding to match PyTorch frame count 1150const int pad_left = hparams .audio_window_len /2 ; 1151const int fft_size = hparams .audio_n_fft ; 1152const int hop = hparams .audio_hop_len ; 1153const int n_with_left = (int )chunk_len + pad_left ; 1154// PyTorch: unfold(size=frame_length+1, step=hop) on semicausal-padded waveform 1155const int64_t pt_frames = (n_with_left - (hparams .audio_window_len + 1 )) /hop + 1 ; 1156const int64_t n_padded_needed = (pt_frames - 1 )* hop + fft_size ; 1157const 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 1161mtmd_audio_mel out_chunk ; 1162bool ok = log_mel_spectrogram (padded_samples .data (),padded_samples .size (),4 ,params ,cache ,out_chunk ); 1163if (!ok ) { 1164return false; 1165 } 1166 1167// Trim to PyTorch frame count 1168out_chunk .n_len = std::min (out_chunk .n_len ,pt_frames ); 1169 1170output .push_back (std::move (out_chunk )); 1171 } 1172 1173return true; 1174} 1175 1176// 1177// mtmd_audio_preprocessor_parakeet implementation 1178// 1179 1180void mtmd_audio_preprocessor_parakeet::worker_thread ( 1181int ith , 1182const float * window_func , 1183int window_size , 1184const std::vector < float > & samples , 1185int n_samples , 1186int frame_size , 1187int frame_step , 1188int n_threads , 1189int n_fft_bins , 1190const mtmd_audio_cache & cache , 1191mtmd_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 1195int n_fb = n_fft_bins ; 1196int i = ith ; 1197 1198GGML_ASSERT (n_fb == 1 + (frame_size /2 )); 1199 1200const double eps = 5.960464477539063e-08 ; 1201 1202for (;i < std::min (n_samples /frame_step + 1 , (int )mel .n_len );i += n_threads ) { 1203const int offset = i * frame_step ; 1204const 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. 1210const int n_to_process = std::min ({window_size ,n_samples - offset }); 1211for (int j = 0 ;j < n_to_process ;j ++ ) { 1212fft_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. 1219fft (cache ,fft_in .data (),frame_size ,fft_out .data ()); 1220 1221// Calculate modulus^2 of complex numbers. 1222for (int j = 0 ;j < n_fb ;j ++ ) { 1223fft_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. 1227for (int j = 0 ;j < mel .n_mel ;j ++ ) { 1228double sum = 0.0 ; 1229int k = 0 ; 1230for (k = 0 ;k < n_fb - 3 ;k += 4 ) { 1231sum += 1232fft_out [k + 0 ]* cache .filters .data [j * n_fb + k + 0 ]+ 1233fft_out [k + 1 ]* cache .filters .data [j * n_fb + k + 1 ]+ 1234fft_out [k + 2 ]* cache .filters .data [j * n_fb + k + 2 ]+ 1235fft_out [k + 3 ]* cache .filters .data [j * n_fb + k + 3 ]; 1236 } 1237for (;k < n_fb ;k ++ ) { 1238sum += fft_out [k ]* cache .filters .data [j * n_fb + k ]; 1239 } 1240mel .data [j * mel .n_len + i ]= std::log (sum + eps ); 1241 } 1242 } 1243 1244// Otherwise fft_out are all zero. 1245const double empty_sum = std::log (eps ); 1246for (;i < mel .n_len ;i += n_threads ) { 1247for (int j = 0 ;j < mel .n_mel ;j ++ ) { 1248mel .data [j * mel .n_len + i ]= empty_sum ; 1249 } 1250 } 1251} 1252 1253void mtmd_audio_preprocessor_parakeet::initialize () { 1254cache .fill_sin_cos_table (hparams .audio_n_fft ); 1255 1256const size_t n_fft = hparams .audio_n_fft /2 + 1 ; 1257GGML_ASSERT (hparams .mel_filters .size ()== (size_t )hparams .n_mel_bins * n_fft ); 1258cache .filters .n_mel = hparams .n_mel_bins ; 1259cache .filters .n_fft = n_fft ; 1260cache .filters .data = hparams .mel_filters ; 1261 1262GGML_ASSERT (hparams .window .size ()== (size_t )hparams .audio_window_len ); 1263GGML_ASSERT (hparams .window .size () <= (size_t )hparams .audio_n_fft ); 1264cache .hann_window = hparams .window ; 1265} 1266 1267bool mtmd_audio_preprocessor_parakeet::preprocess (const float * samples , 1268size_t n_samples_in , 1269 std::vector < mtmd_audio_mel > & output )const { 1270if (n_samples_in == 0 ) { 1271return false; 1272 } 1273 1274filter_params params ; 1275params .n_mel = hparams .n_mel_bins ; 1276params .n_fft_bins = 1 + (hparams .audio_n_fft /2 ); 1277params .hann_window_size = hparams .audio_window_len ; 1278params .hop_length = hparams .audio_hop_len ; 1279params .sample_rate = hparams .audio_sample_rate ; 1280 1281GGML_ASSERT (!cache .sin_vals .empty ()); 1282GGML_ASSERT (!cache .cos_vals .empty ()); 1283GGML_ASSERT (!cache .filters .data .empty ()); 1284 1285const float * window_func = cache .hann_window .data (); 1286const int window_size = params .hann_window_size ; 1287const int frame_size = (params .n_fft_bins - 1 )* 2 ; 1288const 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 { 1293const float preemph = 0.97f ; 1294for (int i = n_samples_in - 1 ;i > 0 ;i -- ) { 1295samples_preprocessed [i ]= samples_preprocessed [i ]- preemph * samples_preprocessed [i - 1 ]; 1296 } 1297 } 1298 1299// Parakeet uses centered constant padding 1300const 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 1304mtmd_audio_mel out_full ; 1305out_full .n_mel = params .n_mel ; 1306out_full .n_len = (samples_padded .size ()- frame_size ) /frame_step + 1 ; 1307out_full .n_len_org = out_full .n_len ; 1308out_full .data .resize (out_full .n_mel * out_full .n_len ); 1309 1310const int n_threads = 4 ; 1311 std::vector < std::thread > workers (n_threads - 1 ); 1312for (int iw = 0 ;iw < n_threads - 1 ;++ iw ) { 1313workers [iw ]= std::thread ( 1314worker_thread ,iw + 1 , 1315window_func , 1316window_size , 1317 std::cref (samples_padded ), 1318samples_padded .size (), 1319frame_size , 1320frame_step , 1321n_threads , 1322params .n_fft_bins , 1323 std::cref (cache ), 1324 std::ref (out_full ) 1325 ); 1326 } 1327 1328worker_thread (0 , 1329window_func , 1330window_size , 1331samples_padded , 1332samples_padded .size (), 1333frame_size , 1334frame_step , 1335n_threads , 1336params .n_fft_bins , 1337cache , 1338out_full ); 1339 1340for (int iw = 0 ;iw < n_threads - 1 ;++ iw ) { 1341workers [iw ].join (); 1342 } 1343 1344// Per-feature normalization (only on valid frames) 1345 { 1346const double eps = 1e-5 ; 1347int valid_frames = n_samples_in /frame_step ; 1348 1349for (int j = 0 ;j < out_full .n_mel ;j ++ ) { 1350double sum = 0.0 ; 1351double sq_diff_sum = 0.0 ; 1352 1353// Calculate Mean ONLY on valid audio frames 1354for (int i = 0 ;i < valid_frames ;i ++ ) { 1355sum += (double )out_full .data [j * out_full .n_len + i ]; 1356 } 1357double mean = sum /valid_frames ; 1358 1359// Calculate Variance ONLY on valid audio frames 1360for (int i = 0 ;i < valid_frames ;i ++ ) { 1361double diff = (double )out_full .data [j * out_full .n_len + i ]- mean ; 1362sq_diff_sum += diff * diff ; 1363 } 1364 1365double std_dev = std::sqrt (sq_diff_sum / (valid_frames - 1.0 )); 1366double denominator = std_dev + eps ; 1367 1368// Apply to ALL frames (including the padded ones) 1369for (int i = 0 ;i < out_full .n_len ;i ++ ) { 1370out_full .data [j * out_full .n_len + i ]= (float )((out_full .data [j * out_full .n_len + i ]- mean ) /denominator ); 1371 } 1372 } 1373 } 1374 1375output .push_back (std::move (out_full )); 1376return 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 , 1388size_t n_samples , 1389 std::vector < mtmd_audio_mel > & output )const { 1390if (n_samples == 0 ) { 1391return false; 1392 } 1393 1394const int frame_size = hparams .n_mel_bins ;// 640 samples per token @ 16 kHz = 40 ms 1395const int n_tokens = ((int )n_samples + frame_size - 1 ) /frame_size ; 1396 1397mtmd_audio_mel mel ; 1398mel .n_len = n_tokens ; 1399mel .n_len_org = n_tokens ; 1400mel .n_mel = frame_size ; 1401mel .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. 1406for (int t = 0 ;t < n_tokens ;t ++ ) { 1407for (int f = 0 ;f < frame_size ;f ++ ) { 1408size_t src = (size_t )t * frame_size + f ; 1409mel .data [(size_t )f * n_tokens + t ]= (src < n_samples ) ?samples [src ] :0.0f ; 1410 } 1411 } 1412 1413output .push_back (std::move (mel )); 1414return 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 ) : 1422n_fft (n_fft ), 1423hop_length (hop_length ), 1424n_fft_bins (n_fft /2 + 1 ), 1425overlap_buffer (n_fft ,0.0f ), 1426window_sum_buffer (n_fft ,0.0f ), 1427padding_to_remove ((n_fft - hop_length ) /2 ), 1428ifft_in (n_fft * 2 * 4 ,0.0f ),// extra space for recursive IFFT 1429ifft_out (n_fft * 2 * 4 ,0.0f ) { 1430GGML_ASSERT (n_fft > 0 && hop_length > 0 && hop_length <=n_fft ); 1431cache .fill_sin_cos_table (n_fft ); 1432cache .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 ); 1438padding_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 1445for (int j = 0 ;j < n_fft_bins ;j ++ ) { 1446ifft_in [j * 2 + 0 ]= frame_spectrum [j * 2 + 0 ]; 1447ifft_in [j * 2 + 1 ]= frame_spectrum [j * 2 + 1 ]; 1448 } 1449 1450// mirror negative frequencies 1451for (int j = 1 ;j < n_fft_bins - 1 ;j ++ ) { 1452int mirror_idx = n_fft - j ; 1453ifft_in [mirror_idx * 2 + 0 ]= ifft_in [j * 2 + 0 ]; 1454ifft_in [mirror_idx * 2 + 1 ]= - ifft_in [j * 2 + 1 ];// conjugate 1455 } 1456 1457ifft (cache ,ifft_in .data (),n_fft ,ifft_out .data ()); 1458 1459// update window sum and overlap buffer 1460for (int j = 0 ;j < n_fft ;j ++ ) { 1461window_sum_buffer [j ]+= cache .hann_window [j ]* cache .hann_window [j ]; 1462overlap_buffer [j ]+= ifft_out [j * 2 ]* cache .hann_window [j ]; 1463 } 1464 1465// extract hop_length samples with normalization 1466for (int i = 0 ;i < hop_length ;i ++ ) { 1467if (window_sum_buffer [i ]> 1e-8f ) { 1468output [i ]= overlap_buffer [i ] /window_sum_buffer [i ]; 1469 }else { 1470output [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 1482int to_remove = std::min (padding_to_remove , (int )output .size ()); 1483padding_to_remove -= to_remove ; 1484output .erase (output .begin (),output .begin ()+ to_remove ); 1485 1486return 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 1494int remaining = n_fft - hop_length ; 1495while (remaining > 0 ) { 1496int chunk_size = std::min (remaining ,hop_length ); 1497 1498for (int i = 0 ;i < chunk_size ;i ++ ) { 1499float sample ; 1500if (window_sum_buffer [i ]> 1e-8f ) { 1501sample = overlap_buffer [i ] /window_sum_buffer [i ]; 1502 }else { 1503sample = overlap_buffer [i ]; 1504 } 1505output .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 1515remaining -= chunk_size ; 1516 } 1517 1518return 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 , 1529size_t n_samples , 1530 std::vector < mtmd_audio_mel > & output )const { 1531// the encoder needs whole frames, see pad_for_conv1d() in the reference 1532const int64_t frame_size = (int64_t )hparams .mimi_downsample * 120 ; 1533if (n_samples == 0 || frame_size <=0 ) { 1534return false; 1535 } 1536 1537// the mimi transformer mask is dense, so cost is quadratic in the reference length 1538const int64_t max_samples = (int64_t ) clip_hparams::pockettts_max_spk_seconds * hparams .audio_sample_rate ; 1539if ((int64_t )n_samples > max_samples ) { 1540LOG_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 ); 1542n_samples = (size_t )max_samples ; 1543 } 1544 1545const int64_t n_frames = (int64_t ) (n_samples + frame_size - 1 ) /frame_size ; 1546const int64_t n_padded = n_frames * frame_size ; 1547 1548mtmd_audio_mel out ; 1549out .n_mel = 1 ; 1550out .n_len = n_padded ; 1551out .n_len_org = (int64_t )n_samples ; 1552out .data .assign ((size_t )n_padded ,0.0f ); 1553 std::copy (samples ,samples + n_samples ,out .data .begin ()); 1554 1555output .push_back (std::move (out )); 1556return true; 1557}