Zum Inhalt

src/audio/bands.ts

// src/audio/bands.ts
// Pure helpers for splitting an FFT magnitude spectrum into bass / mid / high
// energy levels. Kept free of the AudioContext / AnalyserNode dependency so it
// stays unit-testable in the node test environment.

export const BASS_HZ: readonly [number, number] = [20, 250];
export const MID_HZ: readonly [number, number] = [250, 4000];
export const HIGH_HZ: readonly [number, number] = [4000, 16000];

// Maps AnalyserNode dB values (typically −90 .. −20 dBFS for music at speaking
// volume) onto a 0..1 visual-reactivity range.
const DB_FLOOR = -90;
const DB_CEIL = -20;

function clamp01(v: number): number {
  return v < 0 ? 0 : v > 1 ? 1 : v;
}

/** Average dB across the half-open FFT-bin range [loBin, hiBin], inclusive. */
function avgDb(freq: Float32Array, loBin: number, hiBin: number): number {
  const lo = Math.max(0, Math.floor(loBin));
  const hi = Math.min(freq.length - 1, Math.ceil(hiBin));
  if (hi < lo) return DB_FLOOR;
  let sum = 0;
  for (let i = lo; i <= hi; i++) sum += freq[i] ?? DB_FLOOR;
  return sum / (hi - lo + 1);
}

/**
 * Computes the 0..1 reactivity level of one frequency band given a frequency
 * spectrum (AnalyserNode getFloatFrequencyData output, dB) and the sample rate
 * + FFT size that produced it.
 */
export function bandLevel(
  freq: Float32Array,
  sampleRate: number,
  fftSize: number,
  range: readonly [number, number],
): number {
  const binHz = sampleRate / fftSize;
  const lo = range[0] / binHz;
  const hi = range[1] / binHz;
  const db = avgDb(freq, lo, hi);
  return clamp01((db - DB_FLOOR) / (DB_CEIL - DB_FLOOR));
}