mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-07 07:09:15 +00:00
* fix(core): signal beats read the same at any decode rate and refuse a grid in the tracker The tracker resamples every channel to 22.05 kHz and averages their band power, so the CLI's native-rate decode and Studio's AudioContext decode get the same beats, and a mono file and the same sound on two channels get identical ones. The no-steady-beat floor moves from analyzeMusicFromSignal into the tracker, so hyperframes audio, grep_audio and beats share one floor. It is 0.15: above every one of 66 synthetic ambient, speech, scattered-effect and noise clips (max 0.122), where the wrapper's 0.2 also refused a third of GTZAN's produced music. * feat(core): signal downbeats follow chord changes, and sections hear harmony The tracker now keeps a 10 fps chroma beside its band energies. Downbeats weigh where the chords change, over one beat and over two, with the low-end hit, instead of the low end alone. Sections compare timbre and harmony equally. Tuned on GTZAN's dev half: the tempo prior narrows to 0.8 octave, the local tempo stays within 10% of the global one, and beats sit 15 ms after the onset peak, where annotators put them. * chore(core): trim redundant audio analysis comments * refactor(core): keep the rhythm files under their comment share Drops doc comments whose function or field name already says the same thing.
39 lines
1.4 KiB
TypeScript
39 lines
1.4 KiB
TypeScript
/** Band-limited resampling by windowed-sinc interpolation, so analysis reads the same at any decode rate. */
|
|
|
|
const ZERO_CROSSINGS = 8;
|
|
const STEPS_PER_CROSSING = 256;
|
|
/** Cutoff as a share of the lower Nyquist, leaving the window's transition band below it. */
|
|
const CUTOFF = 0.95;
|
|
|
|
const KERNEL = Float64Array.from({ length: ZERO_CROSSINGS * STEPS_PER_CROSSING + 2 }, (_, i) => {
|
|
const x = i / STEPS_PER_CROSSING;
|
|
if (x >= ZERO_CROSSINGS) return 0;
|
|
const sinc = x === 0 ? 1 : Math.sin(Math.PI * x) / (Math.PI * x);
|
|
return sinc * (0.5 + 0.5 * Math.cos((Math.PI * x) / ZERO_CROSSINGS));
|
|
});
|
|
|
|
function kernelAt(x: number): number {
|
|
const at = Math.abs(x) * STEPS_PER_CROSSING;
|
|
const i = Math.floor(at);
|
|
if (i + 1 >= KERNEL.length) return 0;
|
|
const frac = at - i;
|
|
return KERNEL[i]! * (1 - frac) + KERNEL[i + 1]! * frac;
|
|
}
|
|
|
|
export function resample(samples: Float32Array, from: number, to: number): Float32Array {
|
|
if (from === to) return samples;
|
|
const ratio = to / from;
|
|
const scale = Math.min(1, ratio) * CUTOFF;
|
|
const reach = ZERO_CROSSINGS / scale;
|
|
const out = new Float32Array(Math.floor(samples.length * ratio));
|
|
for (let n = 0; n < out.length; n++) {
|
|
const t = n / ratio;
|
|
const last = Math.min(samples.length - 1, Math.floor(t + reach));
|
|
let sum = 0;
|
|
for (let k = Math.max(0, Math.ceil(t - reach)); k <= last; k++)
|
|
sum += samples[k]! * kernelAt((t - k) * scale);
|
|
out[n] = sum * scale;
|
|
}
|
|
return out;
|
|
}
|