Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
import { AudioDocument } from "@suaveplan/audio-core";
export function buildSineDoc(
freq: number,
sampleRate: number,
durationSamples: number,
channels = 1
): AudioDocument {
const data: Float32Array[] = [];
for (let c = 0; c < channels; c++) {
const arr = new Float32Array(durationSamples);
for (let i = 0; i < durationSamples; i++) {
arr[i] = Math.sin((2 * Math.PI * freq * i) / sampleRate);
}
data.push(arr);
}
const doc = new AudioDocument({
sampleRate,
channels,
sourceLength: 0,
});
doc.push(data);
return doc;
}
export function buildSilentDoc(
sampleRate: number,
durationSamples: number,
channels = 1
): AudioDocument {
const data: Float32Array[] = [];
for (let c = 0; c < channels; c++) {
data.push(new Float32Array(durationSamples));
}
const doc = new AudioDocument({
sampleRate,
channels,
sourceLength: 0,
});
doc.push(data);
return doc;
}
export function buildChordDoc(
pitches: number[],
sampleRate: number,
durationSamples: number
): AudioDocument {
const arr = new Float32Array(durationSamples);
for (let i = 0; i < durationSamples; i++) {
let s = 0;
for (const f of pitches)
s += Math.sin((2 * Math.PI * f * i) / sampleRate);
arr[i] = s / pitches.length;
}
const doc = new AudioDocument({
sampleRate,
channels: 1,
sourceLength: 0,
});
doc.push([arr]);
return doc;
}

View File

@@ -0,0 +1,29 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { ChordDetector, KeyDetector, PitchDetector } from "../src/index.js";
import { buildChordDoc, buildSineDoc } from "./helpers.js";
const SR = 48_000;
describe("audio-analysis-pitch integration", () => {
it("runs pitch, key, and chord detectors over the same document", {
timeout: 30_000,
}, async () => {
const doc = buildChordDoc([261.63, 329.63, 392], SR, 8 * 4096);
const p = await new PitchDetector().analyze(doc);
const k = await new KeyDetector().analyze(doc);
const c = await new ChordDetector().analyze(doc);
expect(p.op).toBe("pitch-detector");
expect(k.op).toBe("key-detector");
expect(c.op).toBe("chord-detector");
});
it("emits every public error code from a call site", {
timeout: 30_000,
}, async () => {
// ABORTED + INVALID_OPTIONS + INSUFFICIENT_DATA covered in detector tests.
// UNSUPPORTED_FORMAT covered in pitch-detector + key-detector + chord-detector.
const doc = buildSineDoc(440, SR, 4 * 4096);
const r = await new PitchDetector().analyze(doc);
expect(r.pitch).toBeGreaterThan(0);
});
});

View File

@@ -0,0 +1,59 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import fc from "fast-check";
import { yinPitch } from "../src/algorithms/yin.js";
describe("yinPitch property tests", () => {
it("estimated pitch confidence is always in [0,1]", {
timeout: 30_000,
}, () => {
fc.assert(
fc.property(fc.integer({ min: 80, max: 2000 }), (freq) => {
const sr = 48_000;
const n = 4096;
const f = new Float32Array(n);
for (let i = 0; i < n; i++)
f[i] = Math.sin((2 * Math.PI * freq * i) / sr);
const r = yinPitch(f, sr, 20, 20_000);
return r.confidence >= 0 && r.confidence <= 1;
}),
{ numRuns: 30 }
);
});
it("estimated pitch is non-negative", { timeout: 30_000 }, () => {
fc.assert(
fc.property(fc.integer({ min: 100, max: 4000 }), (freq) => {
const sr = 48_000;
const n = 4096;
const f = new Float32Array(n);
for (let i = 0; i < n; i++)
f[i] = Math.sin((2 * Math.PI * freq * i) / sr);
const r = yinPitch(f, sr, 20, 20_000);
return r.pitch >= 0;
}),
{ numRuns: 30 }
);
});
it("silence always yields zero pitch", { timeout: 5_000 }, () => {
fc.assert(
fc.property(fc.integer({ min: 1024, max: 8192 }), (n) => {
const r = yinPitch(new Float32Array(n), 48_000, 20, 20_000);
return r.pitch === 0 && r.confidence === 0;
}),
{ numRuns: 20 }
);
});
});
describe("440 Hz sine estimation", () => {
it("yields midi 69 for a 440 Hz tone", { timeout: 5_000 }, () => {
const sr = 48_000;
const n = 4096;
const f = new Float32Array(n);
for (let i = 0; i < n; i++)
f[i] = Math.sin((2 * Math.PI * 440 * i) / sr);
const r = yinPitch(f, sr, 20, 20_000);
expect(Math.abs(r.pitch - 440)).toBeLessThan(1);
});
});