// NeMo の AudioToMelSpectrogramPreprocessor(推論時・dither なし)と同じ log メル。 // 11-10 の Parakeet Ultra の測定ページ(test/asr-bench-2026-11-10-eu/browser/harness/app.js)の写しで、メルの本数を引数にした。 // プリエンファシス 0.97 → 両端に 256 の 0 → 窓 400(対称の Hann を 512 の中央)・hop 160 の FFT 512 のパワー → メル(librosa の slaney、fp32) // → log(x + 2^-24) → 特徴ごとに平均と不偏標準偏差(+1e-5)で正規化。フレーム数は詰めない(pad_to 0 / 1)。 // FastConformer(言語別)は 80 本、Parakeet Ultra は 128 本。表(assets/mel{80,128}.bin)は scripts/make_space_assets.py が librosa で作った。 const N_FFT = 512; const HOP = 160; const N_BINS = N_FFT / 2 + 1; const BITREV = new Uint16Array(N_FFT); { const bits = Math.log2(N_FFT); for (let i = 0; i < N_FFT; i++) { let r = 0; for (let b = 0; b < bits; b++) r |= ((i >> b) & 1) << (bits - 1 - b); BITREV[i] = r; } } const TW_RE = new Float64Array(N_FFT / 2); const TW_IM = new Float64Array(N_FFT / 2); for (let i = 0; i < N_FFT / 2; i++) { TW_RE[i] = Math.cos((-2 * Math.PI * i) / N_FFT); TW_IM[i] = Math.sin((-2 * Math.PI * i) / N_FFT); } function fft512(re, im) { for (let i = 0; i < N_FFT; i++) { const j = BITREV[i]; if (j > i) { let t = re[i]; re[i] = re[j]; re[j] = t; t = im[i]; im[i] = im[j]; im[j] = t; } } for (let size = 2; size <= N_FFT; size <<= 1) { const half = size >> 1; const step = N_FFT / size; for (let start = 0; start < N_FFT; start += size) { for (let k = 0; k < half; k++) { const wr = TW_RE[k * step]; const wi = TW_IM[k * step]; const a = start + k; const b = a + half; const xr = re[b] * wr - im[b] * wi; const xi = re[b] * wi + im[b] * wr; re[b] = re[a] - xr; im[b] = im[a] - xi; re[a] += xr; im[a] += xi; } } } } /** * @param {number} nMels 80 か 128 * @param {Float32Array} table [nMels × 257 のフィルタバンク, 512 の窓] * @returns {(audio: Float32Array) => { mel: Float32Array, T: number, seqLen: number }} mel は [nMels × T](T = seqLen + 1) */ export function makeLogMel(nMels, table) { const FB = table.subarray(0, nMels * N_BINS); const WIN = table.subarray(nMels * N_BINS, nMels * N_BINS + N_FFT); // フィルタの 0 でない範囲(0 を足しても和は変わらないので、範囲だけ回して同じ値になる) const lo = new Int32Array(nMels); const hi = new Int32Array(nMels); for (let m = 0; m < nMels; m++) { let a = N_BINS; let b = -1; for (let k = 0; k < N_BINS; k++) { if (FB[m * N_BINS + k] !== 0) { if (a === N_BINS) a = k; b = k; } } lo[m] = a; hi[m] = b + 1; } const re = new Float64Array(N_FFT); const im = new Float64Array(N_FFT); const power = new Float32Array(N_BINS); return (audio) => { const n = audio.length; const seqLen = Math.floor(n / HOP); const x = new Float32Array(n + N_FFT); const pad = N_FFT / 2; if (n > 0) x[pad] = audio[0]; for (let i = 1; i < n; i++) x[pad + i] = Math.fround(audio[i] - Math.fround(0.97 * audio[i - 1])); const frames = 1 + Math.floor((x.length - N_FFT) / HOP); const T = frames; const mel = new Float32Array(nMels * T); for (let f = 0; f < frames; f++) { const off = f * HOP; for (let i = 0; i < N_FFT; i++) { re[i] = Math.fround(x[off + i] * WIN[i]); im[i] = 0; } fft512(re, im); for (let k = 0; k < N_BINS; k++) power[k] = re[k] * re[k] + im[k] * im[k]; for (let m = 0; m < nMels; m++) { const row = m * N_BINS; let s = 0; for (let k = lo[m]; k < hi[m]; k++) s += FB[row + k] * power[k]; mel[m * T + f] = Math.log(Math.fround(s) + 2 ** -24); } } // 特徴ごとの正規化(seqLen フレームの平均と不偏標準偏差 + 1e-5)。seqLen 以降は 0 const L = Math.max(1, seqLen); for (let m = 0; m < nMels; m++) { const row = m * T; let mean = 0; for (let f = 0; f < L; f++) mean += mel[row + f]; mean /= L; let v = 0; for (let f = 0; f < L; f++) v += (mel[row + f] - mean) ** 2; const std = Math.sqrt(v / Math.max(1, L - 1)) + 1e-5; for (let f = 0; f < T; f++) mel[row + f] = f < seqLen ? (mel[row + f] - mean) / std : 0; } return { mel, T, seqLen }; }; }