ryusei-46's picture
端末の検証ページ(tiny・A・B の比較、長時間、Wake Lock、結果の JSON)を追加
3ec1ed2 verified
Raw History Blame Contribute Delete
4.57 kB
// 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 };
};
}