fix: dataset layout data/<tier>/<seed>, mix tests, cli hardening
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
ca9f3bbb83
commit
32d96cadcc
+5
-2
@@ -15,7 +15,10 @@ function parseSeeds(s: string): number[] {
|
||||
|
||||
function arg(args: string[], name: string, def: string): string {
|
||||
const i = args.indexOf(`--${name}`);
|
||||
return i >= 0 && args[i + 1] ? args[i + 1]! : def;
|
||||
if (i < 0) return def;
|
||||
const v = args[i + 1];
|
||||
if (v === undefined || v.startsWith("--")) throw new Error(`missing value for --${name}`);
|
||||
return v;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
@@ -23,7 +26,7 @@ async function main() {
|
||||
switch (cmd) {
|
||||
case "gen": {
|
||||
const tiers = arg(rest, "tiers", "t1a,t1b,t1c,t1d").split(",") as TierId[];
|
||||
for (const t of tiers) if (!(t in TIERS)) throw new Error(`unknown tier: ${t}`);
|
||||
for (const t of tiers) if (!Object.hasOwn(TIERS, t)) throw new Error(`unknown tier: ${t}`);
|
||||
const seeds = parseSeeds(arg(rest, "seeds", "1-5"));
|
||||
const root = arg(rest, "root", "data");
|
||||
for (const t of tiers) for (const s of seeds) {
|
||||
|
||||
@@ -10,16 +10,16 @@ import { mixStemsScaled } from "../audio/mix.ts";
|
||||
import { readWav, writeWav } from "../audio/wav.ts";
|
||||
import { encodeMp3 } from "../audio/mp3.ts";
|
||||
|
||||
const SF2_PATH = "node_modules/@coderline/alphatab/dist/soundfont/sonivox.sf2";
|
||||
const SF2_PATH = join(import.meta.dir, "../../node_modules/@coderline/alphatab/dist/soundfont/sonivox.sf2");
|
||||
const SR = 44100;
|
||||
|
||||
/**
|
||||
* Build one song into `<root>/<tier>-<seed>/`. Idempotent: skipped when mix.mp3 exists and song.json matches.
|
||||
* Build one song into `<root>/<tier>/<seed>/`. Idempotent: skipped when mix.mp3 exists and song.json matches.
|
||||
* stems/<track>.wav are exactly each track's contribution to mix.wav (post-fx, post-pan, gain, normalisation),
|
||||
* so the stems sum to the mix up to PCM16 rounding.
|
||||
*/
|
||||
export async function buildSong(tier: TierId, seed: number, root = "data"): Promise<string> {
|
||||
const dir = join(root, `${tier}-${seed}`);
|
||||
const dir = join(root, tier, String(seed));
|
||||
const song = generateSong(tier, seed);
|
||||
const songJson = JSON.stringify(song, null, 1);
|
||||
const jsonFile = Bun.file(join(dir, "song.json"));
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { test, expect } from "bun:test";
|
||||
import { mixStemsScaled } from "../../src/audio/mix.ts";
|
||||
import type { Audio } from "../../src/core/types.ts";
|
||||
|
||||
test("mixStemsScaled: scaled stems sum to mix; scale is the normalisation factor", () => {
|
||||
const mk = (n: number, amp: number): Audio => {
|
||||
const x = Float32Array.from({ length: n }, (_, i) => amp * Math.sin(i / 7));
|
||||
return { sampleRate: 44100, channels: [x, x.slice()] };
|
||||
};
|
||||
const a = mk(1000, 0.9), b = mk(700, 0.8);
|
||||
const { mix, scaled, scale } = mixStemsScaled([{ audio: a, gainDb: 0 }, { audio: b, gainDb: -3 }]);
|
||||
// unnormalised peak
|
||||
const gb = Math.pow(10, -3 / 20);
|
||||
let peak = 0;
|
||||
for (let i = 0; i < 1000; i++) peak = Math.max(peak, Math.abs(a.channels[0]![i]! + (i < 700 ? b.channels[0]![i]! * gb : 0)));
|
||||
expect(peak).toBeGreaterThan(1);
|
||||
expect(Math.abs(scale - Math.pow(10, -1 / 20) / peak)).toBeLessThan(1e-6);
|
||||
expect(scaled.length).toBe(2);
|
||||
let maxDiff = 0;
|
||||
for (let c = 0; c < 2; c++) for (let i = 0; i < 1000; i++) {
|
||||
const s = scaled[0]!.channels[c]![i]! + scaled[1]!.channels[c]![i]!;
|
||||
maxDiff = Math.max(maxDiff, Math.abs(s - mix.channels[c]![i]!));
|
||||
}
|
||||
expect(maxDiff).toBeLessThan(1e-6);
|
||||
});
|
||||
@@ -35,3 +35,23 @@ test("buildSong writes all artefacts; loadSongDir reads them; stems sum to mix;
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}, 120_000);
|
||||
|
||||
test("multi-track t1b: stems on disk sum to mix.wav", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "stsheets-ds-"));
|
||||
try {
|
||||
const dir = await buildSong("t1b", 1, root);
|
||||
expect(dir).toBe(join(root, "t1b", "1"));
|
||||
const { song, mix, stems } = await loadSongDir(dir);
|
||||
expect(song.tracks.length).toBeGreaterThan(1);
|
||||
expect(stems.size).toBe(song.tracks.length);
|
||||
let maxDiff = 0;
|
||||
for (let c = 0; c < mix.channels.length; c++) for (let i = 0; i < mix.channels[c]!.length; i++) {
|
||||
let s = 0;
|
||||
for (const a of stems.values()) s += a.channels[c]![i] ?? 0;
|
||||
maxDiff = Math.max(maxDiff, Math.abs(s - mix.channels[c]![i]!));
|
||||
}
|
||||
expect(maxDiff).toBeLessThan(2e-3);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}, 120_000);
|
||||
|
||||
Reference in New Issue
Block a user