KoshiMazaki commited on
Commit
30dbda2
·
verified ·
1 Parent(s): 3b309f5

scripts/dataset: add build_eurorack_dry.py

Browse files
Files changed (1) hide show
  1. scripts/dataset/build_eurorack_dry.py +124 -0
scripts/dataset/build_eurorack_dry.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Build one combined dry file to run through the Eurorack in a single pass.
2
+
3
+ WHY ONE FILE: processing every source in one continuous pass guarantees the patch
4
+ settings are identical across all of them. Doing it per-source is exactly what
5
+ broke the outdoor spaces — six sources ended up 0.11-0.33x the ambience level of
6
+ the others because the treatment drifted between renders.
7
+
8
+ GAPS BETWEEN SOURCES: a granular delay has a long, unpredictable tail. Without a
9
+ gap, source N's tail bleeds into source N+1's opening and every slice after the
10
+ first is contaminated. GAP_S of silence lets each tail decay before the next
11
+ source starts. The tail is still ringing at the 6 s mark, which is what the
12
+ dataset wants — clips are hard-cut with no fade, because a tail still sounding at
13
+ the boundary teaches the model that reverb continues past the window.
14
+
15
+ LEVELS: every source is normalised to the same ACTIVE loudness first, so the
16
+ patch sees consistent input. Active rather than peak because claps are sparse —
17
+ they read 12.7 dB quiet on RMS while sounding no quieter.
18
+
19
+ SOURCES: the 10 clean existing sources plus the 4 new recordings. The four
20
+ ElevenLabs sources are excluded — their Prohibited Use Policy bars training on
21
+ output, so they are being replaced by these recordings.
22
+
23
+ Writes the combined wav plus a manifest giving each source's exact offset, so the
24
+ processed return can be sliced back precisely.
25
+ """
26
+ import csv
27
+ import subprocess
28
+ import wave
29
+ import os
30
+ from pathlib import Path
31
+
32
+ import numpy as np
33
+
34
+ SLICED = Path(os.environ.get("AKUSPACE_DATASET", "../../../../AUDIO-LTX-LORA/Dataset")) / "Sliced" / "dry"
35
+ NEW = Path(os.environ.get("AKUSPACE_DATASET", "../../../../AUDIO-LTX-LORA/Dataset")) / "male-female" / "female-male-dry.wav"
36
+ OUT = Path(os.environ.get("AKUSPACE_DATASET", "../../../../AUDIO-LTX-LORA/Dataset")) / "eurorack-input"
37
+ SR, SLOT_S, GAP_S = 48000, 6.0, 6.0
38
+ TARGET_ACTIVE_DB = -16.0
39
+
40
+ # ElevenLabs sources, excluded — being replaced by the new recordings
41
+ DROP = {"femaleTTS_1", "femaleTTS_2", "voice_scot_01", "voice_scot_02"}
42
+ # the new 24 s file is 4 x 6 s: female, female, male, male (iPhone)
43
+ NEW_SLICES = ["female_new_01", "female_new_02", "male_new_01", "male_new_02"]
44
+
45
+
46
+ def load(p, ch=2):
47
+ r = subprocess.run(["ffmpeg", "-v", "error", "-i", str(p), "-ac", str(ch),
48
+ "-ar", str(SR), "-f", "f32le", "-"], capture_output=True)
49
+ return np.frombuffer(r.stdout, dtype="<f4").astype(np.float64).reshape(-1, ch)
50
+
51
+
52
+ def save(x, p):
53
+ p.parent.mkdir(parents=True, exist_ok=True)
54
+ y = np.clip(x, -1.0, 1.0)
55
+ i = (y * 8388607.0).astype(np.int32)
56
+ b = np.stack([i & 0xFF, (i >> 8) & 0xFF, (i >> 16) & 0xFF], axis=-1).astype(np.uint8)
57
+ with wave.open(str(p), "wb") as w:
58
+ w.setnchannels(x.shape[1]); w.setsampwidth(3); w.setframerate(SR)
59
+ w.writeframes(b.tobytes())
60
+
61
+
62
+ def active_db(x):
63
+ m = x.mean(axis=1)
64
+ fr = SR // 20
65
+ n = len(m) // fr
66
+ if n < 5:
67
+ return 20 * np.log10(np.sqrt((m ** 2).mean()) + 1e-12)
68
+ f = np.sqrt((m[:n * fr].reshape(n, fr) ** 2).mean(axis=1))
69
+ a = f[f > f.max() * 0.1]
70
+ return 20 * np.log10(a.mean() + 1e-12) if a.size else -99.0
71
+
72
+
73
+ slot, gap = int(SLOT_S * SR), int(GAP_S * SR)
74
+ items = []
75
+
76
+ for p in sorted(SLICED.glob("*.wav")):
77
+ if p.stem in DROP:
78
+ continue
79
+ items.append((p.stem, load(p)[:slot]))
80
+
81
+ if NEW.exists():
82
+ n = load(NEW)
83
+ for i, name in enumerate(NEW_SLICES):
84
+ seg = n[i * slot:(i + 1) * slot]
85
+ if len(seg) >= slot // 2:
86
+ items.append((name, seg))
87
+ else:
88
+ print(f"!! missing {NEW}")
89
+
90
+ print(f"{'source':<20} {'active in':>10} {'gain':>7} {'offset':>9}")
91
+ print("-" * 52)
92
+ buf, rows, pos = [], [], 0.0
93
+ for name, x in items:
94
+ a = active_db(x)
95
+ g = 10 ** ((TARGET_ACTIVE_DB - a) / 20)
96
+ y = x * g
97
+ if len(y) < slot:
98
+ y = np.vstack([y, np.zeros((slot - len(y), y.shape[1]))])
99
+ buf.append(y)
100
+ buf.append(np.zeros((gap, y.shape[1])))
101
+ rows.append({"source": name, "start_s": f"{pos:.3f}",
102
+ "end_s": f"{pos + SLOT_S:.3f}", "gain_db": f"{20*np.log10(g):+.2f}"})
103
+ print(f"{name:<20} {a:>9.1f} {20*np.log10(g):>+6.1f} {pos:>8.1f}s")
104
+ pos += SLOT_S + GAP_S
105
+
106
+ combined = np.vstack(buf)
107
+ peak = 20 * np.log10(np.abs(combined).max() + 1e-12)
108
+ if peak > -3.0: # uniform trim, changes no ratio
109
+ combined *= 10 ** ((-3.0 - peak) / 20)
110
+ print(f"\n trimmed {-3.0 - peak:+.1f} dB for headroom")
111
+
112
+ save(combined, OUT / "eurorack_input_dry.wav")
113
+ with open(OUT / "eurorack_input_manifest.csv", "w", newline="") as fh:
114
+ w = csv.DictWriter(fh, fieldnames=list(rows[0].keys()))
115
+ w.writeheader(); w.writerows(rows)
116
+
117
+ print(f"\n{len(items)} sources, {SLOT_S:g}s each + {GAP_S:g}s gap")
118
+ print(f" total {len(combined)/SR:.1f}s peak {20*np.log10(np.abs(combined).max()+1e-12):+.1f} dBFS")
119
+ print(f" -> {OUT}/eurorack_input_dry.wav")
120
+ print(f" -> {OUT}/eurorack_input_manifest.csv (offsets for slicing the return)")
121
+ print("\nRun this whole file through the patch in ONE pass. Do not touch the")
122
+ print("controls between sources — the modulation varying is fine and even")
123
+ print("desirable, but the SEND LEVEL must stay constant or you reproduce the")
124
+ print("outdoor bug, where one caption meant different amounts on different sources.")