Initial Commit
This commit is contained in:
63
packages/media/audio/audio-analysis-pitch/tests/helpers.ts
Normal file
63
packages/media/audio/audio-analysis-pitch/tests/helpers.ts
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user