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.
36 lines
1.4 KiB
TypeScript
36 lines
1.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { resample } from "./audioResample";
|
|
|
|
const tone = (hz: number, rate: number, seconds: number) =>
|
|
Float32Array.from({ length: seconds * rate }, (_, i) => Math.sin((2 * Math.PI * hz * i) / rate));
|
|
|
|
const rms = (x: Float32Array) => Math.sqrt(x.reduce((a, v) => a + v * v, 0) / x.length);
|
|
|
|
/** Upward zero crossings per second, over the middle of the signal. */
|
|
function frequency(x: Float32Array, rate: number): number {
|
|
const from = Math.floor(x.length / 4);
|
|
const to = Math.floor((3 * x.length) / 4);
|
|
let crossings = 0;
|
|
for (let i = from + 1; i < to; i++) if (x[i - 1]! < 0 && x[i]! >= 0) crossings++;
|
|
return (crossings * rate) / (to - from);
|
|
}
|
|
|
|
describe("resample", () => {
|
|
it("keeps a tone's pitch and level from 48 kHz to 22.05 kHz", () => {
|
|
const out = resample(tone(1000, 48_000, 2), 48_000, 22_050);
|
|
expect(out.length).toBe(44_100);
|
|
expect(frequency(out, 22_050)).toBeCloseTo(1000, -1);
|
|
expect(rms(out.subarray(1000, -1000))).toBeCloseTo(Math.SQRT1_2, 2);
|
|
});
|
|
|
|
it("removes what the lower rate cannot hold instead of folding it down", () => {
|
|
const out = resample(tone(15_000, 48_000, 1), 48_000, 22_050);
|
|
expect(rms(out.subarray(1000, -1000))).toBeLessThan(0.01);
|
|
});
|
|
|
|
it("returns the samples untouched at the same rate", () => {
|
|
const x = tone(440, 22_050, 1);
|
|
expect(resample(x, 22_050, 22_050)).toBe(x);
|
|
});
|
|
});
|