1/**
2 * Dataset synthesis. Each run: split every type's snippets into a training
3 * set and a placement pool, then place events on a silent timeline. The
4 * overlap knob controls how events land: a "clean" event keeps at least one
5 * snippet-length of separation from everything placed so far, an
6 * "overlapper" is aimed inside the span of an already-placed event. Each
7 * placed snippet gets a random gain in dB, so level carries no information
8 * about identity. Ground truth is exact by construction.
9 */
10import { mulberry32, shuffle, type Rng } from './rng.ts';
11import type { Dataset, GroundTruthEvent, SnippetLibrary, SynthConfig } from './types.ts';
12import { FS } from './types.ts';
14/** Split snippet indices per type into [train, pool]. Guarantees at least
15 * one snippet on each side when a type has two or more. */
16export function splitSnippets(
17 lib: SnippetLibrary,
18 trainFraction: number,
19 rng: Rng,
20): { train: number[][]; pool: number[][] } {
21 const byType: number[][] = lib.names.map(() => []);
22 lib.snippets.forEach((s, i) => byType[s.typeIndex].push(i));
23 const train: number[][] = [];
24 const pool: number[][] = [];
25 for (const idx of byType) {
26 const order = shuffle([...idx], rng);
27 let nTrain = Math.round(order.length * trainFraction);
28 if (order.length >= 2) nTrain = Math.min(Math.max(nTrain, 1), order.length - 1);
29 train.push(order.slice(0, nTrain));
30 pool.push(order.slice(nTrain));
31 }
32 return { train, pool };
33}
35interface Placement { start: number; typeIndex: number; snippetIndex: number; gain: number; }
37export function synthesize(lib: SnippetLibrary, cfg: SynthConfig, runIndex: number): Dataset {
38 const rng = mulberry32(cfg.seed + runIndex);
39 const { train, pool } = splitSnippets(lib, cfg.trainFraction, rng);
40 const snipLen = lib.pre + lib.post;
41 const n = Math.round(cfg.durationS * FS);
42 const margin = snipLen; // keep events off the very edges
44 // Build the shuffled list of events to place: (type, snippet from the
45 // placement pool, gain). Pools are sampled with replacement so
46 // eventsPerType does not depend on how many examples were recorded.
47 const wanted: { typeIndex: number; snippetIndex: number; gain: number }[] = [];
48 for (let k = 0; k < lib.names.length; k++) {
49 if (pool[k].length === 0) continue;
50 for (let e = 0; e < cfg.eventsPerType; e++) {
51 const snippetIndex = pool[k][Math.floor(rng() * pool[k].length)];
52 const gainDb = cfg.gainMinDb * rng();
53 wanted.push({ typeIndex: k, snippetIndex, gain: Math.pow(10, gainDb / 20) });
54 }
55 }
56 shuffle(wanted, rng);
58 const placed: Placement[] = [];
59 const maxTries = 200;
60 for (const w of wanted) {
61 const overlapper = placed.length > 0 && rng() < cfg.overlap;
62 let start = -1;
63 if (overlapper) {
64 const host = placed[Math.floor(rng() * placed.length)];
65 const off = Math.round((rng() - 0.5) * snipLen);
66 const s = host.start + off;
67 if (s >= 0 && s + snipLen <= n) start = s;
68 }
69 if (start < 0) {
70 // Clean placement: rejection-sample a start at least snipLen away
71 // from every placed event (measured start-to-start, which for equal
72 // lengths means no sample overlaps).
73 for (let t = 0; t < maxTries; t++) {
74 const s = margin + Math.floor(rng() * Math.max(1, n - snipLen - 2 * margin));
75 let ok = true;
76 for (const p of placed) {
77 if (Math.abs(p.start - s) < snipLen) { ok = false; break; }
78 }
79 if (ok) { start = s; break; }
80 }
81 }
82 if (start < 0) continue; // timeline too crowded; drop the event
83 placed.push({ start, ...w });
84 }
86 const x = new Float32Array(n);
87 const truth: GroundTruthEvent[] = [];
88 for (const p of placed) {
89 const snip = lib.snippets[p.snippetIndex].samples;
90 for (let i = 0; i < snip.length && p.start + i < n; i++) x[p.start + i] += snip[i] * p.gain;
91 truth.push({ t: (p.start + lib.pre) / FS, typeIndex: p.typeIndex, gain: p.gain });
92 }
93 truth.sort((a, b) => a.t - b.t);
95 // Protect against clipping without rescaling typical data: only scale
96 // down when the mix actually exceeds full scale.
97 let peak = 0;
98 for (let i = 0; i < n; i++) { const a = Math.abs(x[i]); if (a > peak) peak = a; }
99 if (peak > 0.99) { const g = 0.99 / peak; for (let i = 0; i < n; i++) x[i] *= g; }
101 return { x, fs: FS, truth, trainIndices: train };
102}