UNPKG

audio

Version:

Audio loading, editing, and rendering for JavaScript

105 lines (87 loc) 4.4 kB
/** * Mel-frequency spectrum — FFT → mel-binned magnitudes. * Core analysis primitive for spectrum display, spectrogram, feature extraction. * Used by stat system (`a.stat('spectrum')`) and CLI playback visualization. */ import fft from 'fourier-transform' import hann from 'window-function/hann' import { a as aWeight } from 'a-weighting' // ── Mel scale ─────────────────────────────────────────────────── export let toMel = f => 2595 * Math.log10(1 + f / 700) export let fromMel = m => 700 * (10 ** (m / 2595) - 1) // ── Window cache ──────────────────────────────────────────────── let windows = {} function hannWin(n) { if (windows[n]) return windows[n] let w = new Float32Array(n) for (let i = 0; i < n; i++) w[i] = hann(i, n) return (windows[n] = w) } // ── Core ──────────────────────────────────────────────────────── /** * Compute mel-binned magnitude spectrum from a block of samples. * @param {Float32Array} samples — mono PCM block (length should be power of 2) * @param {number} sr — sample rate * @param {object} [opts] * @param {number} [opts.bins=128] — number of mel frequency bins * @param {number} [opts.fMin=30] — minimum frequency Hz * @param {number} [opts.fMax] — maximum frequency Hz (default: min(sr/2, 20000)) * @param {boolean} [opts.weight=true] — apply A-weighting (perceptual loudness) * @returns {Float32Array} magnitude per mel bin (linear scale) */ export function melSpectrum(samples, sr, opts = {}) { let { bins = 128, fMin = 30, fMax = Math.min(sr / 2, 20000), weight = true } = opts let N = samples.length, win = hannWin(N) let buf = new Float32Array(N) for (let i = 0; i < N; i++) buf[i] = samples[i] * win[i] let mag = fft(buf) let mMin = toMel(fMin), mMax = toMel(fMax), binHz = sr / N let out = new Float32Array(bins) for (let b = 0; b < bins; b++) { let f0 = fromMel(mMin + (mMax - mMin) * b / bins) let f1 = fromMel(mMin + (mMax - mMin) * (b + 1) / bins) let k0 = Math.max(1, Math.floor(f0 / binHz)) let k1 = Math.min(mag.length - 1, Math.ceil(f1 / binHz)) let sum = 0, cnt = 0 for (let k = k0; k <= k1; k++) { sum += mag[k] ** 2; cnt++ } let rms = cnt > 0 ? Math.sqrt(sum / cnt) : 0 if (weight) rms *= aWeight((f0 + f1) / 2, sr) out[b] = rms } return out } // ── Block analysis helper ─────────────────────────────────────── /** Stream ch0, buffer remainder, call fn(block, acc) per N-sample block. Returns {acc, cnt}. */ export async function analyzeBlocks(inst, opts, N, bins, fn) { let acc = new Float64Array(bins), cnt = 0, rem = new Float32Array(0) for await (let pcm of inst.stream({ at: opts?.at, duration: opts?.duration })) { let ch0 = pcm[0] if (!ch0 || !ch0.length) continue let input = ch0 if (rem.length) { input = new Float32Array(rem.length + ch0.length) input.set(rem, 0) input.set(ch0, rem.length) } let limit = input.length - (input.length % N) for (let off = 0; off < limit; off += N) { fn(input.subarray(off, off + N), acc); cnt++ } rem = limit < input.length ? input.slice(limit) : new Float32Array(0) } return { acc, cnt } } // ── Stat registration ─────────────────────────────────────────── import audio from '../core.js' /** a.stat('spectrum', {bins}) → average mel spectrum in dB over range */ audio.fn.spectrum = async function(opts) { let bins = opts?.bins ?? 128 let spectOpts = { bins, fMin: opts?.fMin, fMax: opts?.fMax, weight: opts?.weight } let sr = this.sampleRate let { acc, cnt } = await analyzeBlocks(this, opts, 1024, bins, (block, acc) => { let mag = melSpectrum(block, sr, spectOpts) for (let b = 0; b < bins; b++) acc[b] += mag[b] ** 2 }) if (cnt === 0) return new Float32Array(bins) let out = new Float32Array(bins) for (let b = 0; b < bins; b++) out[b] = 20 * Math.log10(Math.sqrt(acc[b] / cnt) + 1e-10) return out }