Files
FluidSimCpp/tools/audio_preview.py

60 lines
2.2 KiB
Python

"""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")