Improve physical geometry, fluid solvers, audio, and profiling
This commit is contained in:
59
tools/audio_preview.py
Normal file
59
tools/audio_preview.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""Create peak-normalized listening copies of PCM16 or float32 capture WAVs.
|
||||
|
||||
Usage: py tools/audio_preview.py out/audio/single-450-baseline
|
||||
Original files retain calibration; preview levels must not be compared by source.
|
||||
"""
|
||||
import argparse
|
||||
import math
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import wave
|
||||
|
||||
|
||||
def preview(path, destination):
|
||||
data = path.read_bytes()
|
||||
if data[:4] != b"RIFF" or data[8:12] != b"WAVE":
|
||||
raise ValueError(f"Not a WAV: {path}")
|
||||
offset = 12
|
||||
fmt = payload = None
|
||||
while offset + 8 <= len(data):
|
||||
tag, size = struct.unpack_from("<4sI", data, offset)
|
||||
chunk = data[offset + 8:offset + 8 + size]
|
||||
if tag == b"fmt ":
|
||||
fmt = struct.unpack_from("<HHIIHH", chunk)
|
||||
elif tag == b"data":
|
||||
payload = chunk
|
||||
offset += 8 + size + size % 2
|
||||
if fmt is None or payload is None:
|
||||
raise ValueError(f"Missing WAV chunks: {path}")
|
||||
encoding, channels, rate, _, _, bits = fmt
|
||||
if (encoding, bits) == (3, 32):
|
||||
values = [x[0] for x in struct.iter_unpack("<f", payload)]
|
||||
elif (encoding, bits) == (1, 16):
|
||||
values = [x[0] / 32768 for x in struct.iter_unpack("<h", payload)]
|
||||
else:
|
||||
raise ValueError(f"Unsupported WAV format: {path}")
|
||||
if not all(math.isfinite(x) for x in values):
|
||||
raise ValueError(f"Nonfinite audio: {path}")
|
||||
peak = max(map(abs, values), default=0)
|
||||
if peak == 0:
|
||||
print(f"{path.name}: silent; no preview")
|
||||
return
|
||||
gain = 0.8 / peak
|
||||
destination.mkdir(parents=True, exist_ok=True)
|
||||
target = destination / (path.stem + "-preview.wav")
|
||||
with wave.open(str(target), "wb") as out:
|
||||
out.setparams((channels, 2, rate, 0, "NONE", "not compressed"))
|
||||
out.writeframes(b"".join(struct.pack("<h", round(x * gain * 32767)) for x in values))
|
||||
print(f"{target.name}: gain {20 * math.log10(gain):+.1f} dB")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("prefix", type=Path)
|
||||
args = parser.parse_args()
|
||||
paths = sorted(args.prefix.parent.glob(args.prefix.name + "-*.wav"))
|
||||
if not paths:
|
||||
parser.error("no matching capture WAVs")
|
||||
for path in paths:
|
||||
preview(path, args.prefix.parent / "previews")
|
||||
Reference in New Issue
Block a user