KoshiMazaki commited on
Commit
a5b26fb
·
verified ·
1 Parent(s): be46bea

scripts/dataset: add build_v5_rooms.py

Browse files
Files changed (1) hide show
  1. scripts/dataset/build_v5_rooms.py +169 -0
scripts/dataset/build_v5_rooms.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Build cathedral and small room at three levels, normalised to the joint batch.
2
+
3
+ Three render batches exist, made in separate sessions at different send settings,
4
+ so the same level word currently means different amounts depending on which batch
5
+ a source came from:
6
+
7
+ cathedral high small high
8
+ joint (8) -19.7 -14.3
9
+ male-female (4) -18.9 -7.7
10
+ claps, old (2) -9.1 -7.4
11
+
12
+ Cathedral: joint and male-female already agree to 0.8 dB; the claps sit 10 dB
13
+ wetter. Small room: male-female and claps agree; the joint batch is 6.6 dB drier.
14
+ Each space has one batch out of step, and a different one each time.
15
+
16
+ TARGET IS THE JOINT BATCH, per Radek. Every source is scaled to the joint median
17
+ for its space and level, so a level word means one measured amount everywhere.
18
+
19
+ BEATS ARE EXCLUDED, as previously decided: they needed the largest corrections and
20
+ they are percussive, where a dense reverb reads very differently from how it reads
21
+ on sustained material.
22
+
23
+ Claps have only ONE rendered level (the old batch). Since they are verified send
24
+ mixes, low and mid are derived offline by scaling R — the method validated at
25
+ -71.6 dB against a real Ableton render, i.e. inaudible.
26
+
27
+ Scaling touches R only, never the dry, so the dry stays a true dry.
28
+ """
29
+ import subprocess
30
+ import wave
31
+ import os
32
+ from pathlib import Path
33
+
34
+ import numpy as np
35
+
36
+ SR, SLOT = 48000, 6 * 48000
37
+ BASE = Path(os.environ.get("AKUSPACE_DATASET", "../../../../AUDIO-LTX-LORA/Dataset"))
38
+ JOINT, MF, SLICED = BASE / "Levels-Reverb/joint-catedral", BASE / "male-female", BASE / "Sliced"
39
+ OUT = BASE / "v5-rooms"
40
+
41
+ JOINT_SRC = ["citola_01", "citola_02", "beat_01", "beat_02",
42
+ "male_voice_01", "male_voice_02", "piano_01", "piano_sax"]
43
+ MF_SRC = ["female_new_01", "female_new_02", "male_new_01", "male_new_02"]
44
+ LEVELS = ["low", "mid", "high"]
45
+ NO_NORMALIZE = {"beat_01", "beat_02"}
46
+ CEILING_DB = -3.0
47
+
48
+ SPACES = {
49
+ "synthetic_cathedral": {
50
+ "joint": ("dry.wav", ["cathedral low.wav", "cathedral mid.wav", "cathedral high.wav"]),
51
+ "mf": ("female-male-dry.wav", ["female-male-cathedral-low.wav",
52
+ "female-male-cathedral-mid.wav", "female-male-cathedral.wav"]),
53
+ "claps_dir": "synthetic_cathedral",
54
+ },
55
+ "small_room_0_67": {
56
+ "joint": ("dry.wav", ["smallroom_low.wav", "smallroom_mid.wav", "smallroom_high.wav"]),
57
+ "mf": ("female-male-dry.wav", ["female-male-small-low.wav",
58
+ "female-male-small-mid.wav", "female-male-small.wav"]),
59
+ "claps_dir": "small_room_0_67",
60
+ },
61
+ }
62
+
63
+
64
+ def load(p, ch=2):
65
+ r = subprocess.run(["ffmpeg", "-v", "error", "-i", str(p), "-ac", str(ch),
66
+ "-ar", str(SR), "-f", "f32le", "-"], capture_output=True)
67
+ return np.frombuffer(r.stdout, dtype="<f4").astype(np.float64).reshape(-1, ch)
68
+
69
+
70
+ def save(x, p):
71
+ p.parent.mkdir(parents=True, exist_ok=True)
72
+ y = np.clip(x, -1.0, 1.0)
73
+ i = (y * 8388607.0).astype(np.int32)
74
+ b = np.stack([i & 0xFF, (i >> 8) & 0xFF, (i >> 16) & 0xFF], axis=-1).astype(np.uint8)
75
+ with wave.open(str(p), "wb") as w:
76
+ w.setnchannels(x.shape[1]); w.setsampwidth(3); w.setframerate(SR)
77
+ w.writeframes(b.tobytes())
78
+
79
+
80
+ def db(x):
81
+ return 20 * np.log10(np.sqrt((x ** 2).mean()) + 1e-12)
82
+
83
+
84
+ def ratio(d, R):
85
+ return db(R.mean(axis=1)) - db(d.mean(axis=1))
86
+
87
+
88
+ # ---- gather every (source, space, level) as dry + R ------------------------
89
+ pairs = {}
90
+ for space, cfg in SPACES.items():
91
+ dj = load(JOINT / cfg["joint"][0])
92
+ wj = [load(JOINT / f) for f in cfg["joint"][1]]
93
+ for i, src in enumerate(JOINT_SRC):
94
+ a, b = i * SLOT, (i + 1) * SLOT
95
+ d = dj[a:b]
96
+ for lv, w in zip(LEVELS, wj):
97
+ pairs[(src, space, lv)] = ("joint", d, w[a:b] - d)
98
+
99
+ dm = load(MF / cfg["mf"][0])
100
+ wm = [load(MF / f) for f in cfg["mf"][1]]
101
+ for i, src in enumerate(MF_SRC):
102
+ a, b = i * SLOT, (i + 1) * SLOT
103
+ d = dm[a:b]
104
+ for lv, w in zip(LEVELS, wm):
105
+ pairs[(src, space, lv)] = ("male-female", d, w[a:b] - d)
106
+
107
+ # claps: one rendered level, treated as "high"; low/mid derived after scaling
108
+ for src in ("claps_rhythm", "claps_single"):
109
+ dp, wp = SLICED / "dry" / f"{src}.wav", SLICED / cfg["claps_dir"] / f"{src}.wav"
110
+ if dp.exists() and wp.exists():
111
+ d, w = load(dp)[:SLOT], load(wp)[:SLOT]
112
+ n = min(len(d), len(w))
113
+ pairs[(src, space, "high")] = ("claps", d[:n], w[:n] - d[:n])
114
+
115
+ # ---- joint medians are the target ------------------------------------------
116
+ targets = {}
117
+ for space in SPACES:
118
+ for lv in LEVELS:
119
+ vals = [ratio(d, R) for (s, sp, l), (b, d, R) in pairs.items()
120
+ if sp == space and l == lv and b == "joint"]
121
+ targets[(space, lv)] = float(np.median(vals))
122
+ print("targets (joint medians):")
123
+ for (space, lv), t in sorted(targets.items()):
124
+ print(f" {space:<22} {lv:<5} {t:>+7.1f} dB")
125
+
126
+ # ---- normalise, derive missing clap levels, write ---------------------------
127
+ print(f"\n{'source':<16} {'space':<22} {'level':<5} {'was':>8} {'corr':>7} {'now':>8}")
128
+ print("-" * 74)
129
+ out, corrections = {}, []
130
+ for space in SPACES:
131
+ for src in JOINT_SRC + MF_SRC + ["claps_rhythm", "claps_single"]:
132
+ base = pairs.get((src, space, "high"))
133
+ if base is None:
134
+ continue
135
+ batch, d, R_high = base
136
+ for lv in LEVELS:
137
+ key = (src, space, lv)
138
+ if key in pairs:
139
+ _, d, R = pairs[key]
140
+ else:
141
+ # derive from high by the ratio the joint targets imply
142
+ R = R_high * 10 ** ((targets[(space, lv)] - targets[(space, "high")]) / 20)
143
+ was = ratio(d, R)
144
+ if src in NO_NORMALIZE:
145
+ corr = 0.0
146
+ else:
147
+ corr = targets[(space, lv)] - was
148
+ R = R * 10 ** (corr / 20)
149
+ out[key] = (d, R)
150
+ if abs(corr) > 0.05:
151
+ corrections.append(abs(corr))
152
+ print(f"{src:<16} {space:<22} {lv:<5} {was:>+7.1f} {corr:>+6.1f} "
153
+ f"{ratio(d, R):>+7.1f}" + (" (beats: left as rendered)" if src in NO_NORMALIZE else ""))
154
+
155
+ # uniform trim so nothing clips; changes no ratio anywhere
156
+ peak = max(20 * np.log10(np.abs(d + R).max() + 1e-12) for d, R in out.values())
157
+ trim = min(0.0, CEILING_DB - peak)
158
+ print(f"\nloudest {peak:+.1f} dBFS -> uniform trim {trim:+.1f} dB (ceiling {CEILING_DB:+g})")
159
+
160
+ for (src, space, lv), (d, R) in out.items():
161
+ g = 10 ** (trim / 20)
162
+ save((d + R) * g, OUT / space / lv / f"{src}.wav")
163
+ save(d * g, OUT / space / "dry" / f"{src}.wav")
164
+
165
+ n = len(out)
166
+ print(f"\n{n} pairs -> {OUT}")
167
+ print(f" {len(set(k[0] for k in out))} sources x {len(SPACES)} spaces x {len(LEVELS)} levels")
168
+ print(f" corrections applied: {len(corrections)}, median {np.median(corrections):.1f} dB, "
169
+ f"max {max(corrections):.1f} dB")