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

scripts/dataset: add build_v5_outdoor.py

Browse files
Files changed (1) hide show
  1. scripts/dataset/build_v5_outdoor.py +124 -0
scripts/dataset/build_v5_outdoor.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Add the two outdoor places to v5-rooms — de-ghosted, beds exactly as rendered.
2
+
3
+ These are PLACES, not spaces — an ambience bed rather than a reverb. The bed is
4
+ never scaled in either direction: boosting a bed amplifies the field recording's
5
+ own noise floor (tried; sounded unnatural), and pulling everything down to the
6
+ quietest loses the space. Two levels only, cut downward from the render: "high"
7
+ is the bed as rendered, "low" 8 dB under it.
8
+
9
+ THE DE-GHOST: the old render batches carry the dry at 0.74x (day) / 0.67x
10
+ (night) for the eight old non-clap sources — a flat level offset from
11
+ render-batch drift, while claps and the four new voices sit at 1.00. Building
12
+ R = w - d straight from those renders buries a negative ghost of the dry inside
13
+ R, which dominated every naive ambience measurement (the ghost alone reads
14
+ -9.5 dB in night, which is why the old sources "measured" -9.4) and taught two
15
+ transforms under one caption: "duck the voice ~3 dB + bed" on eight sources,
16
+ "keep the voice + bed" on six. So the dry is projected out of each render and
17
+ every pair is rebuilt on a unity dry: bed = w - fit*d, wet = d + bed. True bed
18
+ levels are consistent across sources (day -11..-24, night -19..-28); the
19
+ "17 dB spread" and "claps 10 dB low" stories were artifacts of the ghost.
20
+
21
+ Chosen in the 08-13 A/B (Dataset/v5-outdoor-compare/): both versions sound
22
+ near-identical by ear, so the de-ghosted one wins on consistency — all 28
23
+ outdoor pairs teach the same transform, matching every room and SFX pair.
24
+ """
25
+ import subprocess
26
+ import wave
27
+ import os
28
+ from pathlib import Path
29
+
30
+ import numpy as np
31
+
32
+ SR, SLOT = 48000, 6 * 48000
33
+ BASE = Path(os.environ.get("AKUSPACE_DATASET", "../../../../AUDIO-LTX-LORA/Dataset"))
34
+ SLICED, MF, OUT = BASE / "Sliced", BASE / "male-female", BASE / "v5-rooms"
35
+ DROP = {"femaleTTS_1", "femaleTTS_2", "voice_scot_01", "voice_scot_02"}
36
+ MF_SRC = ["female_new_01", "female_new_02", "male_new_01", "male_new_02"]
37
+ CEILING_DB = -3.0
38
+ LOW_OFFSET_DB = -8.0 # "low" sits this far under "high"
39
+
40
+ SPACES = {
41
+ "outdoor_day_birds": ("outdoor_day_birds", "female-male-outside.wav"),
42
+ "outdoor_night": ("outdoor_night", "female-male-outside-night.wav"),
43
+ }
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 db(x):
63
+ return 20 * np.log10(np.sqrt((x ** 2).mean()) + 1e-12)
64
+
65
+
66
+ base, batch = {}, {}
67
+ dmf = load(MF / "female-male-dry.wav")
68
+ for space, (folder, mffile) in SPACES.items():
69
+ for p in sorted((SLICED / "dry").glob("*.wav")):
70
+ src = p.stem
71
+ if src in DROP:
72
+ continue
73
+ wp = SLICED / folder / f"{src}.wav"
74
+ if not wp.exists():
75
+ continue
76
+ d, w = load(p)[:SLOT], load(wp)
77
+ n = min(len(d), len(w), SLOT)
78
+ base[(src, space)] = (d[:n], w[:n] - d[:n])
79
+ batch[(src, space)] = "existing"
80
+ mp = MF / mffile
81
+ if mp.exists():
82
+ w = load(mp)
83
+ for i, src in enumerate(MF_SRC):
84
+ a, b = i * SLOT, (i + 1) * SLOT
85
+ d, ws = dmf[a:b], w[a:b]
86
+ n = min(len(d), len(ws))
87
+ base[(src, space)] = (d[:n], ws[:n] - d[:n])
88
+ batch[(src, space)] = "new"
89
+
90
+ print(f"{'source':<16} {'space':<20} {'batch':<10} {'fit':>6} {'naive':>8} {'bed':>8}")
91
+ print("-" * 76)
92
+ out = {}
93
+ for space in SPACES:
94
+ for (src, sp), (d, R) in sorted(base.items()):
95
+ if sp != space:
96
+ continue
97
+ naive = db(R.mean(axis=1)) - db(d.mean(axis=1))
98
+ # project the dry out of the render; fit = 1 + c, with c the ghost
99
+ # coefficient (about -0.33 on the night old sources, ~0 on claps and
100
+ # the new voices). The residual is the true bed, kept as rendered —
101
+ # the bed itself is never scaled up or down.
102
+ c = float(np.dot(R.ravel(), d.ravel()) / (np.dot(d.ravel(), d.ravel()) + 1e-12))
103
+ bed = R - c * d
104
+ out[(src, space, "high")] = (d, bed)
105
+ out[(src, space, "low")] = (d, bed * 10 ** (LOW_OFFSET_DB / 20))
106
+ print(f"{src:<16} {space:<20} {batch[(src, sp)]:<10} {1 + c:>6.3f} "
107
+ f"{naive:>+7.1f} {db(bed.mean(axis=1)) - db(d.mean(axis=1)):>+7.1f}")
108
+ print(f"{'':<16} {space:<20} beds as rendered on a unity dry; low = high {LOW_OFFSET_DB:+g} dB\n")
109
+
110
+ peak = max(20 * np.log10(np.abs(d + R).max() + 1e-12) for d, R in out.values())
111
+ trim = min(0.0, CEILING_DB - peak)
112
+ print(f"loudest {peak:+.1f} dBFS -> uniform trim {trim:+.1f} dB")
113
+ for (src, space, lv), (d, R) in out.items():
114
+ g = 10 ** (trim / 20)
115
+ save((d + R) * g, OUT / space / lv / f"{src}.wav")
116
+ save(d * g, OUT / space / "dry" / f"{src}.wav")
117
+
118
+ import shutil
119
+ for space in SPACES: # drop the old single-level folder
120
+ old = OUT / space / "bed"
121
+ if old.exists():
122
+ shutil.rmtree(old)
123
+
124
+ print(f"\n{len(out)} pairs -> {OUT} ({len(SPACES)} places x 2 levels)")